]> sigrok.org Git - pulseview.git/commitdiff
Backport recent changes from mainline.
authorUwe Hermann <redacted>
Tue, 31 Mar 2020 20:35:18 +0000 (22:35 +0200)
committerUwe Hermann <redacted>
Tue, 31 Mar 2020 20:35:18 +0000 (22:35 +0200)
This includes all changes from

  a7f4a81bd96dc9e2095bdb44c0d6276375533bb6
  manual: Bump date to the date of the last change.

up to

  7bcd627e2945c193d3a8bee7089ec1e2fab89eac
  cmake: Don't do a QUIET search for libsigrokcxx

115 files changed:
CMakeLists.txt
INSTALL
README
contrib/org.sigrok.PulseView.appdata.xml
contrib/pulseview_cross.nsi.in
doc/pulseview.1
icons/edit-paste.svg [new file with mode: 0644]
icons/search.svg [new file with mode: 0644]
l10n/de.ts [new file with mode: 0644]
main.cpp
manual/acquisition.txt
manual/analysis.txt
manual/cli.txt
manual/decoders.txt
manual/installation.txt
manual/overview.txt
pulseview.qrc
pv/application.cpp
pv/application.hpp
pv/binding/binding.cpp
pv/binding/decoder.cpp
pv/data/decode/annotation.cpp
pv/data/decode/annotation.hpp
pv/data/decode/decoder.cpp
pv/data/decode/decoder.hpp
pv/data/decode/row.cpp
pv/data/decode/row.hpp
pv/data/decode/rowdata.cpp
pv/data/decode/rowdata.hpp
pv/data/decodesignal.cpp
pv/data/decodesignal.hpp
pv/data/logicsegment.cpp
pv/data/signalbase.cpp
pv/devicemanager.cpp
pv/dialogs/connect.cpp
pv/dialogs/connect.hpp
pv/dialogs/settings.cpp
pv/dialogs/settings.hpp
pv/dialogs/storeprogress.cpp
pv/globalsettings.cpp
pv/globalsettings.hpp
pv/logging.cpp
pv/mainwindow.cpp
pv/mainwindow.hpp
pv/popups/channels.cpp
pv/popups/channels.hpp
pv/session.cpp
pv/session.hpp
pv/storesession.cpp
pv/subwindows/decoder_selector/item.cpp [new file with mode: 0644]
pv/subwindows/decoder_selector/model.cpp [new file with mode: 0644]
pv/subwindows/decoder_selector/subwindow.cpp [new file with mode: 0644]
pv/subwindows/decoder_selector/subwindow.hpp [new file with mode: 0644]
pv/subwindows/subwindowbase.cpp [new file with mode: 0644]
pv/subwindows/subwindowbase.hpp [new file with mode: 0644]
pv/toolbars/mainbar.cpp
pv/toolbars/mainbar.hpp
pv/util.cpp
pv/util.hpp
pv/views/decoder_binary/QHexView.cpp [new file with mode: 0644]
pv/views/decoder_binary/QHexView.hpp [new file with mode: 0644]
pv/views/decoder_binary/view.cpp [new file with mode: 0644]
pv/views/decoder_binary/view.hpp [new file with mode: 0644]
pv/views/trace/analogsignal.cpp
pv/views/trace/analogsignal.hpp
pv/views/trace/cursor.cpp
pv/views/trace/cursor.hpp
pv/views/trace/cursorpair.cpp
pv/views/trace/cursorpair.hpp
pv/views/trace/decodetrace.cpp
pv/views/trace/decodetrace.hpp
pv/views/trace/flag.cpp
pv/views/trace/flag.hpp
pv/views/trace/header.cpp
pv/views/trace/logicsignal.cpp
pv/views/trace/logicsignal.hpp
pv/views/trace/marginwidget.cpp
pv/views/trace/marginwidget.hpp
pv/views/trace/rowitem.cpp [deleted file]
pv/views/trace/rowitem.hpp [deleted file]
pv/views/trace/ruler.cpp
pv/views/trace/ruler.hpp
pv/views/trace/signal.cpp
pv/views/trace/signal.hpp
pv/views/trace/standardbar.cpp
pv/views/trace/timeitem.cpp
pv/views/trace/timeitem.hpp
pv/views/trace/timemarker.cpp
pv/views/trace/timemarker.hpp
pv/views/trace/trace.cpp
pv/views/trace/tracetreeitem.hpp
pv/views/trace/tracetreeitemowner.cpp
pv/views/trace/triggermarker.cpp
pv/views/trace/triggermarker.hpp
pv/views/trace/view.cpp
pv/views/trace/view.hpp
pv/views/trace/viewitem.cpp
pv/views/trace/viewitem.hpp
pv/views/trace/viewport.cpp
pv/views/trace/viewwidget.cpp
pv/views/trace/viewwidget.hpp
pv/views/viewbase.cpp
pv/views/viewbase.hpp
pv/widgets/decodermenu.cpp
pv/widgets/decodermenu.hpp
pv/widgets/flowlayout.cpp [new file with mode: 0644]
pv/widgets/flowlayout.hpp [new file with mode: 0644]
pv/widgets/importmenu.cpp
pv/widgets/importmenu.hpp
pv/widgets/popup.cpp
pv/widgets/popup.hpp
pv/widgets/timestampspinbox.cpp
pv/widgets/wellarray.hpp
test/CMakeLists.txt
translations.qrc [new file with mode: 0644]

index a19593d7219f73663b7e88da6797bf4e2e54473c..79d3a69c16c3f8837bc141afd20f07acc4449dba 100644 (file)
@@ -3,6 +3,7 @@
 ##
 ## Copyright (C) 2012 Joel Holdsworth <joel@airwebreathe.org.uk>
 ## Copyright (C) 2012-2013 Alexandru Gagniuc <mr.nuke.me@gmail.com>
+## Copyright (C) 2020 Soeren Apel <soeren@apelpie.net>
 ##
 ## 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
@@ -44,6 +45,7 @@ option(DISABLE_WERROR "Build without -Werror" TRUE)
 option(ENABLE_SIGNALS "Build with UNIX signals" TRUE)
 option(ENABLE_STACKTRACE "Enable stack trace when crashing" FALSE)
 option(ENABLE_DECODE "Build with libsigrokdecode" TRUE)
+option(ENABLE_FLOW "Build with libsigrokflow" FALSE)
 option(ENABLE_TESTS "Enable unit tests" FALSE)
 option(STATIC_PKGDEPS_LIBS "Statically link to (pkg-config) libraries" FALSE)
 
@@ -75,6 +77,11 @@ add_subdirectory(manual)
 list(APPEND PKGDEPS glib-2.0>=2.28.0)
 list(APPEND PKGDEPS glibmm-2.4>=2.28.0)
 
+if(ENABLE_FLOW)
+       list(APPEND PKGDEPS gstreamermm-1.0>=1.8.0)
+       list(APPEND PKGDEPS libsigrokflow>=0.1.0)
+endif()
+
 set(LIBSR_CXX_BINDING "libsigrokcxx>=0.5.1")
 list(APPEND PKGDEPS "${LIBSR_CXX_BINDING}")
 
@@ -87,7 +94,7 @@ if(ANDROID)
 endif()
 
 find_package(PkgConfig)
-pkg_check_modules(LIBSRCXX QUIET ${LIBSR_CXX_BINDING})
+pkg_check_modules(LIBSRCXX ${LIBSR_CXX_BINDING})
 if(NOT LIBSRCXX_FOUND OR NOT LIBSRCXX_VERSION)
        message(FATAL_ERROR "libsigrok C++ bindings missing, check libsigrok's 'configure' output (missing dependencies?)")
 endif()
@@ -95,14 +102,16 @@ pkg_check_modules(PKGDEPS REQUIRED ${PKGDEPS})
 
 set(CMAKE_AUTOMOC TRUE)
 
-find_package(Qt5 COMPONENTS Core Gui Widgets Svg REQUIRED)
+find_package(Qt5 5.3 COMPONENTS Core Gui LinguistTools Widgets Svg REQUIRED)
+
+message(STATUS "Qt version: ${Qt5_VERSION}")
 
 if(WIN32)
        # MXE workaround: Use pkg-config to find Qt5 libs.
        # https://github.com/mxe/mxe/issues/1642
        # Not required (and doesn't work) on MSYS2.
        if(NOT DEFINED ENV{MSYSTEM})
-               pkg_check_modules(QT5ALL REQUIRED Qt5Widgets Qt5Gui Qt5Svg)
+               pkg_check_modules(QT5ALL REQUIRED Qt5Widgets>=5.3 Qt5Gui>=5.3 Qt5Svg>=5.3)
        endif()
 endif()
 
@@ -260,6 +269,7 @@ set(pulseview_SOURCES
        pv/prop/int.cpp
        pv/prop/property.cpp
        pv/prop/string.cpp
+       pv/subwindows/subwindowbase.cpp
        pv/toolbars/mainbar.cpp
        pv/views/trace/analogsignal.cpp
        pv/views/trace/cursor.cpp
@@ -268,7 +278,6 @@ set(pulseview_SOURCES
        pv/views/trace/header.cpp
        pv/views/trace/marginwidget.cpp
        pv/views/trace/logicsignal.cpp
-       pv/views/trace/rowitem.cpp
        pv/views/trace/ruler.cpp
        pv/views/trace/signal.cpp
        pv/views/trace/timeitem.cpp
@@ -291,6 +300,7 @@ set(pulseview_SOURCES
        pv/widgets/colorpopup.cpp
        pv/widgets/devicetoolbutton.cpp
        pv/widgets/exportmenu.cpp
+       pv/widgets/flowlayout.cpp
        pv/widgets/importmenu.cpp
        pv/widgets/popup.cpp
        pv/widgets/popuptoolbutton.cpp
@@ -324,6 +334,7 @@ set(pulseview_HEADERS
        pv/prop/int.hpp
        pv/prop/property.hpp
        pv/prop/string.hpp
+       pv/subwindows/subwindowbase.hpp
        pv/toolbars/mainbar.hpp
        pv/views/trace/analogsignal.hpp
        pv/views/trace/cursor.hpp
@@ -331,7 +342,6 @@ set(pulseview_HEADERS
        pv/views/trace/header.hpp
        pv/views/trace/logicsignal.hpp
        pv/views/trace/marginwidget.hpp
-       pv/views/trace/rowitem.hpp
        pv/views/trace/ruler.hpp
        pv/views/trace/signal.hpp
        pv/views/trace/timeitem.hpp
@@ -350,6 +360,7 @@ set(pulseview_HEADERS
        pv/widgets/colorpopup.hpp
        pv/widgets/devicetoolbutton.hpp
        pv/widgets/exportmenu.hpp
+       pv/widgets/flowlayout.hpp
        pv/widgets/importmenu.hpp
        pv/widgets/popup.hpp
        pv/widgets/popuptoolbutton.hpp
@@ -375,6 +386,11 @@ if(ENABLE_DECODE)
                pv/data/decode/decoder.cpp
                pv/data/decode/row.cpp
                pv/data/decode/rowdata.cpp
+               pv/subwindows/decoder_selector/item.cpp
+               pv/subwindows/decoder_selector/model.cpp
+               pv/subwindows/decoder_selector/subwindow.cpp
+               pv/views/decoder_binary/view.cpp
+               pv/views/decoder_binary/QHexView.cpp
                pv/views/trace/decodetrace.cpp
                pv/widgets/decodergroupbox.cpp
                pv/widgets/decodermenu.cpp
@@ -382,6 +398,9 @@ if(ENABLE_DECODE)
 
        list(APPEND pulseview_HEADERS
                pv/data/decodesignal.hpp
+               pv/subwindows/decoder_selector/subwindow.hpp
+               pv/views/decoder_binary/view.hpp
+               pv/views/decoder_binary/QHexView.hpp
                pv/views/trace/decodetrace.hpp
                pv/widgets/decodergroupbox.hpp
                pv/widgets/decodermenu.hpp
@@ -404,6 +423,21 @@ endif()
 
 qt5_add_resources(pulseview_RESOURCES_RCC ${pulseview_RESOURCES})
 
+#===============================================================================
+#= Translations
+#-------------------------------------------------------------------------------
+
+file(GLOB TS_FILES ${CMAKE_SOURCE_DIR}/l10n/*.ts)
+set_property(SOURCE ${TS_FILES} PROPERTY OUTPUT_LOCATION ${CMAKE_BINARY_DIR}/l10n)
+if (NOT CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR)
+       configure_file("translations.qrc" "translations.qrc" COPYONLY)
+endif ()
+
+qt5_add_translation(QM_FILES ${TS_FILES})
+qt5_create_translation(QM_FILES ${pulseview_SOURCES} ${TS_FILES})
+
+qt5_add_resources(pulseview_RESOURCES_RCC ${CMAKE_BINARY_DIR}/translations.qrc)
+
 #===============================================================================
 #= Global Definitions
 #-------------------------------------------------------------------------------
@@ -414,6 +448,10 @@ add_definitions(-Wall -Wextra)
 add_definitions(-std=c++11)
 add_definitions(-DBOOST_MATH_DISABLE_FLOAT128=1)
 
+if(ENABLE_FLOW)
+       add_definitions(-DENABLE_FLOW)
+endif()
+
 if(ENABLE_DECODE)
        add_definitions(-DENABLE_DECODE)
 endif()
@@ -490,10 +528,11 @@ if(ANDROID)
        list(APPEND PULSEVIEW_LINK_LIBS "-llog")
 endif()
 
+set(INPUT_FILES_LIST ${pulseview_SOURCES} ${pulseview_RESOURCES_RCC} ${QM_FILES})
 if(ANDROID)
-       add_library(${PROJECT_NAME} SHARED ${pulseview_SOURCES} ${pulseview_RESOURCES_RCC})
+       add_library(${PROJECT_NAME} SHARED ${INPUT_FILES_LIST})
 else()
-       add_executable(${PROJECT_NAME} ${pulseview_SOURCES} ${pulseview_RESOURCES_RCC})
+       add_executable(${PROJECT_NAME} ${INPUT_FILES_LIST})
 endif()
 
 target_link_libraries(${PROJECT_NAME} ${PULSEVIEW_LINK_LIBS})
@@ -524,7 +563,7 @@ install(FILES icons/pulseview.png DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons
 install(FILES icons/pulseview.svg DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps)
 
 # Generate Windows installer script.
-configure_file(contrib/pulseview_cross.nsi.in contrib/pulseview_cross.nsi @ONLY)
+configure_file(contrib/pulseview_cross.nsi.in ${CMAKE_CURRENT_BINARY_DIR}/contrib/pulseview_cross.nsi @ONLY)
 
 #===============================================================================
 #= Packaging (handled by CPack)
diff --git a/INSTALL b/INSTALL
index edb7940d522c9126e1edcbf8579e1d54fd2762f7..d666f0a7f8247aa68117b2be16a8822369c59c3f 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -15,8 +15,9 @@ Requirements
  - cmake >= 2.8.12
  - libglib >= 2.28.0
  - glibmm-2.4 (>= 2.28.0)
- - Qt5 (including the following components):
-    - Qt5Core, Qt5Gui, Qt5Widgets, Qt5Svg
+ - Qt5 (>= 5.3), including the following components:
+    - Qt5Core, Qt5Gui, Qt5Widgets, Qt5Svg, Qt5LinguistTools
+    - Qt translation package (optional; needed at runtime, not build time)
  - libboost >= 1.55 (including the following libs):
     - libboost-system
     - libboost-filesystem
diff --git a/README b/README
index 8608b3bf7314024ff15b5c0f0e13eca510bde5b9..a35cb8d58c09c610aa51eee3ced239701c22e32b 100644 (file)
--- a/README
+++ b/README
@@ -22,9 +22,9 @@ PulseView is licensed under the terms of the GNU General Public License
 (GPL), version 3 or later.
 
 While some individual source code files are licensed under the GPLv2+, and
-some files are licensed under the GPLv3+, this doesn't change the fact that
-the program as a whole is licensed under the terms of the GPLv3+ (e.g. also
-due to the fact that it links against GPLv3+ libraries).
+some files are licensed under the GPLv3+ or MIT, this doesn't change the fact
+that the program as a whole is licensed under the terms of the GPLv3+ (e.g.
+also due to the fact that it links against GPLv3+ libraries).
 
 Please see the individual source files for the full list of copyright holders.
 
@@ -47,6 +47,24 @@ is to be interpreted as
 Resource authors and licenses
 -----------------------------
 
+icons/application-exit.png,
+icons/document-new.png,
+icons/document-open.png,
+icons/document-save-as.png,
+icons/edit-paste.svg,
+icons/help-browser.png,
+icons/media-playback-pause.png,
+icons/media-playback-start.png,
+icons/preferences-system.png,
+icons/search.svg,
+icons/window-new.png,
+icons/zoom-fit-best.png,
+icons/zoom-in.png,
+icons/zoom-out.png: Tango Icon Library
+  http://tango.freedesktop.org/Tango_Desktop_Project
+  License:
+    Public Domain
+
 icons/information.svg: Bobarino
   https://en.wikipedia.org/wiki/File:Information.svg
   License:
@@ -65,6 +83,12 @@ DarkStyle: Juergen Skrotzky
     MIT license
     https://github.com/Jorgen-VikingGod/Qt-Frameless-Window-DarkStyle#licence
 
+QHexView:
+  https://github.com/virinext/QHexView
+  License:
+    MIT license
+    https://github.com/virinext/QHexView/blob/master/LICENSE
+
 
 Mailing list
 ------------
index 48a5397139cba4dbcc293a853297322d8696828f..5097897f16d6b317d5c8bd9a8d0763d6fa704f31 100644 (file)
@@ -7,20 +7,19 @@
   <name>PulseView</name>
   <summary>Logic analyzer, oscilloscope and MSO GUI</summary>
   <description>
-    <p>PulseView is a Qt based logic analyzer, oscilloscope and MSO GUI for sigrok.</p>
-    <p>Features:</p>
+    <p>PulseView is a Qt based logic analyzer, oscilloscope and MSO GUI for sigrok supporting various features:</p>
     <ul>
       <li>Fast O(log N) signal rendering at all zoom levels</li>
       <li>Protocol decoder support</li>
       <li>Trace groups support</li>
     </ul>
   </description>
-  <url type="homepage">http://sigrok.org/wiki/PulseView</url>
-  <url type="bugtracker">http://sigrok.org/bugzilla/enter_bug.cgi?format=guided&amp;product=PulseView</url>
-  <url type="faq">http://sigrok.org/wiki/FAQ</url>
+  <url type="homepage">https://sigrok.org/wiki/PulseView</url>
+  <url type="bugtracker">https://sigrok.org/bugzilla/enter_bug.cgi?format=guided&amp;product=PulseView</url>
+  <url type="faq">https://sigrok.org/wiki/FAQ</url>
   <screenshots>
     <screenshot type="default">
-      <image>http://sigrok.org/wimg/e/ee/PulseView_I2C_DS1307_Decode.png</image>
+      <image>https://sigrok.org/wimg/e/ee/PulseView_I2C_DS1307_Decode.png</image>
     </screenshot>
   </screenshots>
   <provides>
index fdcdf49ebf667e406192de01869e04d65c30d885..88945f44b214a5e7be774fb016b6c156ef8a265e 100644 (file)
@@ -1,7 +1,7 @@
 ##
 ## This file is part of the PulseView project.
 ##
-## Copyright (C) 2013-2014 Uwe Hermann <uwe@hermann-uwe.de>
+## Copyright (C) 2013-2020 Uwe Hermann <uwe@hermann-uwe.de>
 ##
 ## This program is free software; you can redistribute it and/or modify
 ## it under the terms of the GNU General Public License as published by
@@ -160,6 +160,7 @@ Section "PulseView (required)" Section1
        # Python
        File "${CROSS}/python34.dll"
        File "${CROSS}/python34.zip"
+       File "${CROSS}/*.pyd"
 
        SetOutPath "$INSTDIR\share"
 
@@ -272,6 +273,7 @@ Section "Uninstall"
        Delete "$INSTDIR\zadig_xp.exe"
        Delete "$INSTDIR\python34.dll"
        Delete "$INSTDIR\python34.zip"
+       Delete "$INSTDIR\*.pyd"
 
        # Delete all decoders and everything else in libsigrokdecode/.
        # There could be *.pyc files or __pycache__ subdirs and so on.
index 8289eaa85f858becb08069c3b67a4e742105402c..9663fdf94f3ba5387c66e31d87468b457c15f484 100644 (file)
@@ -1,4 +1,4 @@
-.TH PULSEVIEW 1 "March 30, 2018"
+.TH PULSEVIEW 1 "March 31, 2020"
 .SH "NAME"
 PulseView \- Qt-based LA/scope/MSO GUI for sigrok
 .SH "SYNOPSIS"
@@ -64,6 +64,10 @@ file.
 .BR "\-I, \-\-input\-format " <format>
 Specifies the format of the input file to be loaded.
 .TP
+.BR "\-s, \-\-settings " <filename>
+Load PulseView session setup to use with the input file. The setup file must be
+in the "PulseView session setup" format (.pvs).
+.TP
 .BR "\-c, \-\-clean"
 Prevents the previously used sessions to be restored from settings storage.
 This is useful if you want only a single session with the file given on the
@@ -73,9 +77,6 @@ command line instead of restoring all previously used sessions as well.
 .B "f"
 Zoom-to-fit.
 .TP
-.B "o"
-Zoom 1:1.
-.TP
 .B "s"
 Enable / disable sticky scrolling. When enabled, the right edge of the screen
 always shows the most recently captured data.
@@ -89,6 +90,9 @@ Show / hide analog minor grid (in addition to the vdiv grid).
 .B "c"
 Show / hide cursors.
 .TP
+.B "d"
+Show / hide protocol decoder selector.
+.TP
 .B "b"
 Toggle between coloured trace backgrounds and alternating light/dark
 gray trace backgrounds.
@@ -96,8 +100,20 @@ gray trace backgrounds.
 .B "SPACE"
 Start / stop an acquisition.
 .TP
-.B "Arrow keys"
-Scroll up/down/left/right.
+.B "Left/right arrow keys"
+Scroll left/right.
+.TP
+.B "+/-"
+Zoom in/out.
+.TP
+.B "Up/down arrow keys"
+Zoom in/out.
+.TP
+.B "Home/End"
+Jump to the start/end of the sample data.
+.TP
+.B "1/2"
+Attach left/right side of the cursors to the mouse.
 .TP
 .B "CTRL+o"
 Open file.
@@ -114,11 +130,8 @@ Group all currently selected traces into a trace group.
 .B "CTRL+u"
 Ungroup the traces in the currently selected trace group.
 .TP
-.B "CTRL++"
-Zoom in.
-.TP
-.B "CTRL+-"
-Zoom out.
+.B "CTRL+up/down arrow keys"
+Scroll down/up.
 .TP
 .B "CTRL+q"
 Quit, i.e. shutdown PulseView (closing all session tabs).
diff --git a/icons/edit-paste.svg b/icons/edit-paste.svg
new file mode 100644 (file)
index 0000000..39150d7
--- /dev/null
@@ -0,0 +1,531 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="48px"
+   height="48px"
+   id="svg12360"
+   sodipodi:version="0.32"
+   inkscape:version="0.46"
+   sodipodi:docbase="/home/jimmac/src/cvs/tango-icon-theme/scalable/actions"
+   sodipodi:docname="edit-paste.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape">
+  <defs
+     id="defs12362">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 24 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="48 : 24 : 1"
+       inkscape:persp3d-origin="24 : 16 : 1"
+       id="perspective80" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient5060"
+       id="radialGradient5031"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(-2.774389,0,0,1.969706,112.7623,-872.8854)"
+       cx="605.71429"
+       cy="486.64789"
+       fx="605.71429"
+       fy="486.64789"
+       r="117.14286" />
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient5060">
+      <stop
+         style="stop-color:black;stop-opacity:1;"
+         offset="0"
+         id="stop5062" />
+      <stop
+         style="stop-color:black;stop-opacity:0;"
+         offset="1"
+         id="stop5064" />
+    </linearGradient>
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient5060"
+       id="radialGradient5029"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(2.774389,0,0,1.969706,-1891.633,-872.8854)"
+       cx="605.71429"
+       cy="486.64789"
+       fx="605.71429"
+       fy="486.64789"
+       r="117.14286" />
+    <linearGradient
+       id="linearGradient5048">
+      <stop
+         style="stop-color:black;stop-opacity:0;"
+         offset="0"
+         id="stop5050" />
+      <stop
+         id="stop5056"
+         offset="0.5"
+         style="stop-color:black;stop-opacity:1;" />
+      <stop
+         style="stop-color:black;stop-opacity:0;"
+         offset="1"
+         id="stop5052" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient5048"
+       id="linearGradient5027"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(2.774389,0,0,1.969706,-1892.179,-872.8854)"
+       x1="302.85715"
+       y1="366.64789"
+       x2="302.85715"
+       y2="609.50507" />
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient2259">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop2261" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop2263" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient2251">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop2253" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop2255" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient2239">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop2241" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop2243" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient2224">
+      <stop
+         style="stop-color:#7c7c7c;stop-opacity:1;"
+         offset="0"
+         id="stop2226" />
+      <stop
+         style="stop-color:#b8b8b8;stop-opacity:1;"
+         offset="1"
+         id="stop2228" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient2216">
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="0"
+         id="stop2218" />
+      <stop
+         style="stop-color:#000000;stop-opacity:0;"
+         offset="1"
+         id="stop2220" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient15234">
+      <stop
+         style="stop-color:#97978a;stop-opacity:1.0000000;"
+         offset="0.0000000"
+         id="stop15236" />
+      <stop
+         id="stop15242"
+         offset="0.50000000"
+         style="stop-color:#c2c2b9;stop-opacity:1.0000000;" />
+      <stop
+         style="stop-color:#7d7d6f;stop-opacity:1.0000000;"
+         offset="1.0000000"
+         id="stop15238" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient15218">
+      <stop
+         style="stop-color:#f0f0ef;stop-opacity:1.0000000;"
+         offset="0.0000000"
+         id="stop15220" />
+      <stop
+         id="stop2269"
+         offset="0.59928656"
+         style="stop-color:#e8e8e8;stop-opacity:1;" />
+      <stop
+         id="stop2267"
+         offset="0.82758623"
+         style="stop-color:#ffffff;stop-opacity:1;" />
+      <stop
+         style="stop-color:#d8d8d3;stop-opacity:1.0000000;"
+         offset="1.0000000"
+         id="stop15222" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient14484">
+      <stop
+         style="stop-color:#c68827;stop-opacity:1.0000000;"
+         offset="0.0000000"
+         id="stop14486" />
+      <stop
+         style="stop-color:#89601f;stop-opacity:1.0000000;"
+         offset="1.0000000"
+         id="stop14488" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient13746">
+      <stop
+         style="stop-color:#646459;stop-opacity:1;"
+         offset="0"
+         id="stop13748" />
+      <stop
+         style="stop-color:#646459;stop-opacity:0;"
+         offset="1"
+         id="stop13750" />
+    </linearGradient>
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient13746"
+       id="radialGradient13752"
+       cx="24.647722"
+       cy="45.272587"
+       fx="24.647722"
+       fy="45.272587"
+       r="21.011173"
+       gradientTransform="matrix(1.000000,0.000000,0.000000,0.110577,4.987330e-17,40.26648)"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient14484"
+       id="linearGradient14490"
+       x1="6.1071744"
+       y1="10.451290"
+       x2="33.857143"
+       y2="37.879860"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient15218"
+       id="linearGradient15224"
+       x1="22.308331"
+       y1="18.992140"
+       x2="35.785294"
+       y2="39.498238"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.065698,0.000000,0.000000,0.987595,-1.564439,7.487332e-2)" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient15234"
+       id="linearGradient15240"
+       x1="25.404572"
+       y1="3.8180194"
+       x2="25.464211"
+       y2="9.3233509"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.052632,0.000000,0.000000,1.000000,-1.789474,0.000000)" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2216"
+       id="linearGradient2222"
+       x1="36.8125"
+       y1="39.15625"
+       x2="39.0625"
+       y2="42.0625"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2224"
+       id="linearGradient2230"
+       x1="35.996582"
+       y1="40.458221"
+       x2="33.664921"
+       y2="37.770721"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2239"
+       id="linearGradient2245"
+       x1="25.682829"
+       y1="12.172059"
+       x2="25.692169"
+       y2="-0.20294096"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2251"
+       id="linearGradient2257"
+       x1="33.396004"
+       y1="36.921333"
+       x2="34.170048"
+       y2="38.070381"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2259"
+       id="linearGradient2265"
+       x1="26.076092"
+       y1="26.696676"
+       x2="30.811172"
+       y2="42.007351"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient15234"
+       id="linearGradient2283"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.538743,0.000000,0.000000,0.511806,10.80080,-0.582640)"
+       x1="25.404572"
+       y1="3.8180194"
+       x2="25.404572"
+       y2="6.481061" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient15234"
+       id="linearGradient2287"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.005222,0.000000,0.000000,0.883928,-0.627923,0.843750)"
+       x1="25.404572"
+       y1="3.8180194"
+       x2="25.464211"
+       y2="9.3233509" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="0.15294118"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="-125.14425"
+     inkscape:cy="14.046835"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:grid-bbox="true"
+     inkscape:document-units="px"
+     inkscape:window-width="872"
+     inkscape:window-height="818"
+     inkscape:window-x="385"
+     inkscape:window-y="30"
+     inkscape:showpageshadow="false" />
+  <metadata
+     id="metadata12365">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title>Edit Paste</dc:title>
+        <dc:date>2005-10-10</dc:date>
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Andreas Nilsson</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <dc:subject>
+          <rdf:Bag>
+            <rdf:li>edit</rdf:li>
+            <rdf:li>paste</rdf:li>
+          </rdf:Bag>
+        </dc:subject>
+        <cc:license
+           rdf:resource="http://creativecommons.org/licenses/publicdomain/" />
+        <dc:contributor>
+          <cc:Agent>
+            <dc:title>Jakub Steiner</dc:title>
+          </cc:Agent>
+        </dc:contributor>
+      </cc:Work>
+      <cc:License
+         rdf:about="http://creativecommons.org/licenses/publicdomain/">
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Reproduction" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Distribution" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
+      </cc:License>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1"
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer">
+    <g
+       style="display:inline"
+       id="g5022"
+       transform="matrix(2.325216e-2,0,0,1.485743e-2,44.80627,43.06039)">
+      <rect
+         y="-150.69685"
+         x="-1559.2523"
+         height="478.35718"
+         width="1339.6335"
+         id="rect4173"
+         style="opacity:0.40206185;color:black;fill:url(#linearGradient5027);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+      <path
+         sodipodi:nodetypes="cccc"
+         id="path5058"
+         d="M -219.61876,-150.68038 C -219.61876,-150.68038 -219.61876,327.65041 -219.61876,327.65041 C -76.744594,328.55086 125.78146,220.48075 125.78138,88.454235 C 125.78138,-43.572302 -33.655436,-150.68036 -219.61876,-150.68038 z "
+         style="opacity:0.40206185;color:black;fill:url(#radialGradient5029);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+      <path
+         style="opacity:0.40206185;color:black;fill:url(#radialGradient5031);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+         d="M -1559.2523,-150.68038 C -1559.2523,-150.68038 -1559.2523,327.65041 -1559.2523,327.65041 C -1702.1265,328.55086 -1904.6525,220.48075 -1904.6525,88.454235 C -1904.6525,-43.572302 -1745.2157,-150.68036 -1559.2523,-150.68038 z "
+         id="path5018"
+         sodipodi:nodetypes="cccc" />
+    </g>
+    <rect
+       style="opacity:1.0000000;fill:url(#linearGradient14490);fill-opacity:1.0;fill-rule:evenodd;stroke:#714c16;stroke-width:0.99999976;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4.0000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1"
+       id="rect12368"
+       width="39.035683"
+       height="41.045437"
+       x="4.4643173"
+       y="4.5000000"
+       rx="1.3879371"
+       ry="1.3879364" />
+    <rect
+       style="opacity:1;fill:url(#linearGradient15224);fill-opacity:1;fill-rule:evenodd;stroke:#888a85;stroke-width:1.00000024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect12413"
+       width="30.951559"
+       height="35.976688"
+       x="8.5323219"
+       y="6.5295157"
+       rx="0.56650835"
+       ry="0.56650835" />
+    <rect
+       style="opacity:1.0000000;fill:#5c5c5c;fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:1.0000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4.0000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000"
+       id="rect13756"
+       width="12.000000"
+       height="4.0000000"
+       x="18.000000"
+       y="0.0000000"
+       rx="0.98387533"
+       ry="0.98387533" />
+    <rect
+       style="opacity:1;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient2265);stroke-width:1.00000048;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect15244"
+       width="29.014147"
+       height="34.040764"
+       x="9.5171413"
+       y="7.4665856"
+       rx="0"
+       ry="0" />
+    <rect
+       style="opacity:1.0000000;fill:none;fill-opacity:1.0000000;fill-rule:evenodd;stroke:#c68827;stroke-width:0.99999976;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4.0000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000"
+       id="rect15974"
+       width="37.085655"
+       height="39.092987"
+       x="5.4393425"
+       y="5.4307775"
+       rx="0.47879848"
+       ry="0.47879848" />
+    <rect
+       ry="1.3879364"
+       rx="1.3879377"
+       y="4.4722719"
+       x="14.791488"
+       height="7"
+       width="18.947376"
+       id="rect2208"
+       style="opacity:0.10795455;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000012;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <rect
+       ry="1.3879364"
+       rx="1.3879377"
+       y="3.5000000"
+       x="14.526322"
+       height="7.0000000"
+       width="18.947376"
+       id="rect2285"
+       style="opacity:1.0000000;fill:url(#linearGradient15240);fill-opacity:1.0000000;fill-rule:evenodd;stroke:#5c5c5c;stroke-width:1.0000001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4.0000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000" />
+    <rect
+       ry="0.32543635"
+       rx="0.32543635"
+       y="1.2086792"
+       x="19.151323"
+       height="3.5826404"
+       width="9.6973763"
+       id="rect2281"
+       style="opacity:1;fill:url(#linearGradient2283);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.00000012;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <rect
+       style="opacity:1;fill:url(#linearGradient2287);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.00000012;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect13754"
+       width="18.093992"
+       height="6.1875"
+       x="14.953014"
+       y="3.9375"
+       rx="1.0129364"
+       ry="1.0129364" />
+    <path
+       style="opacity:0.48863636;color:#000000;fill:url(#linearGradient2222);fill-opacity:1.0;fill-rule:evenodd;stroke:none;stroke-width:1.00000024;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+       d="M 39.018306,36.25 L 39.0625,42.0625 L 30.5625,42.018306 L 39.018306,36.25 z "
+       id="path2212"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="opacity:1;color:#000000;fill:url(#linearGradient2230);fill-opacity:1.0;fill-rule:evenodd;stroke:#868a84;stroke-width:1.00000024;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+       d="M 30.059082,42.086864 C 31.850223,42.254516 39.048809,37.717278 39.539922,33.698855 C 37.97666,36.121952 34.584971,35.667446 30.476213,35.826456 C 30.476213,35.826456 30.871582,41.586864 30.059082,42.086864 z "
+       id="path2210"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="opacity:0.31681817;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient2245);stroke-width:0.99999994;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 19.46875 1.46875 C 19.466654 1.4708456 19.470414 1.4975336 19.46875 1.5 C 19.46758 1.502776 19.438116 1.4969757 19.4375 1.5 L 19.4375 4.375 C 19.4375 4.3814229 19.46641 4.4006981 19.46875 4.40625 C 19.471216 4.4079135 19.465974 4.4363298 19.46875 4.4375 L 15.9375 4.4375 C 15.91974 4.4375 15.892285 4.4357551 15.875 4.4375 C 15.840968 4.4426713 15.781454 4.4572762 15.75 4.46875 C 15.611832 4.5269964 15.482328 4.6677699 15.4375 4.8125 C 15.426991 4.8535348 15.4375 4.9243489 15.4375 4.96875 L 15.4375 9.125 C 15.4375 9.1427605 15.435755 9.1702147 15.4375 9.1875 C 15.442671 9.2215321 15.457276 9.2810456 15.46875 9.3125 C 15.478458 9.3355281 15.487176 9.3851004 15.5 9.40625 C 15.5046 9.41307 15.526336 9.4309205 15.53125 9.4375 C 15.552124 9.4628138 15.599686 9.5103764 15.625 9.53125 C 15.638159 9.5410789 15.6734 9.5539504 15.6875 9.5625 C 15.702038 9.5703781 15.734648 9.5872782 15.75 9.59375 C 15.781454 9.6052238 15.840968 9.6198287 15.875 9.625 C 15.892285 9.6267449 15.91974 9.625 15.9375 9.625 L 32.0625 9.625 C 32.08026 9.625 32.107715 9.6267449 32.125 9.625 C 32.159032 9.6198287 32.218546 9.6052238 32.25 9.59375 C 32.265352 9.5872782 32.297962 9.5703781 32.3125 9.5625 C 32.3266 9.5539504 32.361841 9.5410789 32.375 9.53125 C 32.400314 9.5103764 32.447876 9.4628138 32.46875 9.4375 C 32.473664 9.4309205 32.4954 9.41307 32.5 9.40625 C 32.512824 9.3851004 32.521542 9.3355281 32.53125 9.3125 C 32.542724 9.2810456 32.557329 9.2215321 32.5625 9.1875 C 32.564245 9.1702147 32.5625 9.1427605 32.5625 9.125 L 32.5625 4.96875 C 32.5625 4.9243489 32.573009 4.8535348 32.5625 4.8125 C 32.517672 4.6677698 32.388168 4.5269964 32.25 4.46875 C 32.218546 4.4572762 32.159032 4.4426713 32.125 4.4375 C 32.107715 4.4357551 32.08026 4.4375 32.0625 4.4375 L 28.53125 4.4375 C 28.534026 4.4363298 28.528784 4.4079135 28.53125 4.40625 C 28.533591 4.4006981 28.5625 4.3814229 28.5625 4.375 L 28.5625 1.5 C 28.561884 1.4969757 28.53242 1.502776 28.53125 1.5 C 28.529586 1.4975336 28.533346 1.4708456 28.53125 1.46875 C 28.528474 1.4675798 28.503024 1.4693657 28.5 1.46875 L 19.5 1.46875 C 19.496976 1.4693657 19.471526 1.4675798 19.46875 1.46875 z "
+       id="rect2232" />
+    <path
+       sodipodi:nodetypes="cccc"
+       id="path2247"
+       d="M 31.509519,40.68705 C 32.879298,40.003221 36.038783,38.086016 37.338164,36.205012 C 35.545641,36.581496 34.347243,36.794585 31.610576,36.900494 C 31.610576,36.900494 31.697137,39.91208 31.509519,40.68705 z "
+       style="opacity:0.36931818;color:#000000;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient2257);stroke-width:0.99999982;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+    <rect
+       style="opacity:0.17045455;color:#000000;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+       id="rect2271"
+       width="21"
+       height="2"
+       x="14"
+       y="15" />
+    <rect
+       y="19"
+       x="14"
+       height="2"
+       width="20"
+       id="rect2273"
+       style="opacity:0.17045455;color:#000000;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+    <rect
+       style="opacity:0.17045455;color:#000000;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+       id="rect2275"
+       width="18"
+       height="2"
+       x="14"
+       y="23" />
+    <rect
+       y="27"
+       x="14"
+       height="2"
+       width="21"
+       id="rect2277"
+       style="opacity:0.17045455;color:#000000;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+    <rect
+       style="opacity:0.17045455;color:#000000;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+       id="rect2279"
+       width="13"
+       height="2"
+       x="14"
+       y="31" />
+  </g>
+</svg>
diff --git a/icons/search.svg b/icons/search.svg
new file mode 100644 (file)
index 0000000..1a4c1cd
--- /dev/null
@@ -0,0 +1,313 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   sodipodi:docname="system-search.svg"
+   sodipodi:docbase="/home/tigert/cvs/freedesktop.org/tango-icon-theme/scalable/actions"
+   inkscape:version="0.46"
+   sodipodi:version="0.32"
+   id="svg11300"
+   height="48px"
+   width="48px"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape">
+  <defs
+     id="defs3">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 24 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="48 : 24 : 1"
+       inkscape:persp3d-origin="24 : 16 : 1"
+       id="perspective47" />
+    <linearGradient
+       id="linearGradient2846">
+      <stop
+         id="stop2848"
+         offset="0.0000000"
+         style="stop-color:#8a8a8a;stop-opacity:1.0000000;" />
+      <stop
+         id="stop2850"
+         offset="1.0000000"
+         style="stop-color:#484848;stop-opacity:1.0000000;" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient2366">
+      <stop
+         id="stop2368"
+         offset="0"
+         style="stop-color:#ffffff;stop-opacity:1;" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0.21904762;"
+         offset="0.50000000"
+         id="stop2374" />
+      <stop
+         id="stop2370"
+         offset="1.0000000"
+         style="stop-color:#ffffff;stop-opacity:1.0000000;" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient4487">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop4489" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop4491" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient4477">
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="0"
+         id="stop4479" />
+      <stop
+         style="stop-color:#000000;stop-opacity:0;"
+         offset="1"
+         id="stop4481" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient4467">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop4469" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0.24761905;"
+         offset="1.0000000"
+         id="stop4471" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient4454">
+      <stop
+         style="stop-color:#729fcf;stop-opacity:0.20784314;"
+         offset="0.0000000"
+         id="stop4456" />
+      <stop
+         style="stop-color:#729fcf;stop-opacity:0.67619050;"
+         offset="1.0000000"
+         id="stop4458" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient4440">
+      <stop
+         style="stop-color:#7d7d7d;stop-opacity:1;"
+         offset="0"
+         id="stop4442" />
+      <stop
+         id="stop4448"
+         offset="0.50000000"
+         style="stop-color:#b1b1b1;stop-opacity:1.0000000;" />
+      <stop
+         style="stop-color:#686868;stop-opacity:1.0000000;"
+         offset="1.0000000"
+         id="stop4444" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient4440"
+       id="linearGradient4446"
+       x1="30.656250"
+       y1="34.000000"
+       x2="33.218750"
+       y2="31.062500"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.334593,0.000000,0.000000,1.291292,-6.973842,-7.460658)" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient4454"
+       id="radialGradient4460"
+       cx="18.240929"
+       cy="21.817987"
+       fx="18.240929"
+       fy="21.817987"
+       r="8.3085051"
+       gradientUnits="userSpaceOnUse" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient4467"
+       id="radialGradient4473"
+       cx="15.414371"
+       cy="13.078408"
+       fx="15.414371"
+       fy="13.078408"
+       r="6.6562500"
+       gradientTransform="matrix(2.592963,-7.746900e-24,-5.714443e-24,2.252104,-25.05975,-18.94100)"
+       gradientUnits="userSpaceOnUse" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient4487"
+       id="radialGradient4493"
+       cx="24.130018"
+       cy="37.967922"
+       fx="24.130018"
+       fy="37.967922"
+       r="16.528622"
+       gradientTransform="matrix(1.000000,0.000000,0.000000,0.237968,3.152859e-15,28.93278)"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       gradientUnits="userSpaceOnUse"
+       y2="25.743469"
+       x2="17.500893"
+       y1="13.602121"
+       x1="18.292673"
+       id="linearGradient2372"
+       xlink:href="#linearGradient2366"
+       inkscape:collect="always" />
+    <radialGradient
+       r="16.528622"
+       fy="37.967922"
+       fx="24.130018"
+       cy="37.967922"
+       cx="24.130018"
+       gradientTransform="matrix(1.000000,0.000000,0.000000,0.237968,-2.471981e-16,28.93278)"
+       gradientUnits="userSpaceOnUse"
+       id="radialGradient2842"
+       xlink:href="#linearGradient4477"
+       inkscape:collect="always" />
+    <linearGradient
+       gradientUnits="userSpaceOnUse"
+       y2="30.557772"
+       x2="31.335964"
+       y1="26.580296"
+       x1="27.366341"
+       id="linearGradient2852"
+       xlink:href="#linearGradient2846"
+       inkscape:collect="always" />
+  </defs>
+  <sodipodi:namedview
+     stroke="#3465a4"
+     inkscape:window-y="94"
+     inkscape:window-x="239"
+     inkscape:window-height="754"
+     inkscape:window-width="691"
+     inkscape:showpageshadow="false"
+     inkscape:document-units="px"
+     inkscape:grid-bbox="true"
+     showgrid="false"
+     inkscape:current-layer="layer1"
+     inkscape:cy="23.07052"
+     inkscape:cx="9.502648"
+     inkscape:zoom="11.313708"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     borderopacity="0.25490196"
+     bordercolor="#666666"
+     pagecolor="#ffffff"
+     id="base"
+     fill="#729fcf" />
+  <metadata
+     id="metadata4">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Jakub Steiner</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <dc:source>http://jimmac.musichall.cz</dc:source>
+        <cc:license
+           rdf:resource="http://creativecommons.org/licenses/publicdomain/" />
+      </cc:Work>
+      <cc:License
+         rdf:about="http://creativecommons.org/licenses/publicdomain/">
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Reproduction" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Distribution" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
+      </cc:License>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:groupmode="layer"
+     inkscape:label="Layer 1"
+     id="layer1">
+    <g
+       id="g1772">
+      <path
+         sodipodi:type="arc"
+         style="opacity:0.17112298;color:#000000;fill:url(#radialGradient2842);fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10.000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;visibility:visible;display:inline;overflow:visible;"
+         id="path4475"
+         sodipodi:cx="24.130018"
+         sodipodi:cy="37.967922"
+         sodipodi:rx="16.528622"
+         sodipodi:ry="3.9332814"
+         d="M 40.658640 37.967922 A 16.528622 3.9332814 0 1 1  7.6013966,37.967922 A 16.528622 3.9332814 0 1 1  40.658640 37.967922 z"
+         transform="matrix(1.446431,0.000000,0.000000,1.519990,-10.97453,-17.75168)" />
+      <path
+         sodipodi:nodetypes="csscccscccscczzzz"
+         id="path2844"
+         d="M 18.627569,3.1435548 C 10.488439,3.1435548 3.8827682,9.7492259 3.8827682,17.888356 C 3.8827682,26.027486 10.488439,32.633158 18.627569,32.633158 C 22.107124,32.633158 25.178570,31.248765 27.701292,29.230511 C 27.495915,30.237392 27.623257,31.265879 28.457436,31.990436 L 39.421520,41.517846 C 40.654936,42.589175 42.508982,42.448806 43.580310,41.215389 C 44.651638,39.981971 44.511269,38.127927 43.277853,37.056599 L 32.313769,27.529188 C 31.642242,26.945909 30.820891,26.773219 30.007531,26.886466 C 31.994231,24.374044 33.372370,21.337663 33.372370,17.888356 C 33.372370,9.7492259 26.766699,3.1435548 18.627569,3.1435548 z M 18.551954,4.3697381 C 26.191413,4.3697381 31.843729,9.1586886 31.843729,17.661513 C 31.843729,26.336626 26.027039,30.953288 18.551954,30.953288 C 11.249005,30.953288 5.2601806,25.475196 5.2601806,17.661513 C 5.2601806,9.6774061 11.084819,4.3697380 18.551954,4.3697381 z "
+         style="opacity:1.0000000;color:#000000;fill:#dcdcdc;fill-opacity:1.0000000;fill-rule:evenodd;stroke:url(#linearGradient2852);stroke-width:2.0000010;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10.000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;visibility:visible;display:inline;overflow:visible;" />
+      <path
+         style="opacity:1.0000000;color:#000000;fill:#dcdcdc;fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:1.0000004;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10.000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;visibility:visible;display:inline;overflow:visible;"
+         d="M 18.602905,3.0803551 C 10.437465,3.0803551 3.8104408,9.7073791 3.8104408,17.872819 C 3.8104408,26.038259 10.437465,32.665283 18.602905,32.665283 C 22.093708,32.665283 25.175082,31.276416 27.705960,29.251638 C 27.499919,30.261774 27.627672,31.293585 28.464547,32.020484 L 39.464073,41.578691 C 40.701476,42.653483 42.561515,42.512661 43.636306,41.275256 C 44.711097,40.037852 44.570274,38.177814 43.332871,37.103023 L 32.333346,27.544815 C 31.659648,26.959651 30.835642,26.786402 30.019653,26.900016 C 32.012775,24.379472 33.395369,21.333276 33.395369,17.872819 C 33.395369,9.7073791 26.768345,3.0803551 18.602905,3.0803551 z M 18.527046,6.2664243 C 24.808154,6.2664245 29.905864,11.364135 29.905864,17.645243 C 29.905864,23.926351 24.808154,29.024061 18.527046,29.024061 C 12.245938,29.024061 7.1482276,23.926351 7.1482276,17.645243 C 7.1482278,11.364135 12.245938,6.2664243 18.527046,6.2664243 z "
+         id="path4430" />
+      <path
+         style="opacity:1.0000000;color:#000000;fill:url(#linearGradient4446);fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10.000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;visibility:visible;display:inline;overflow:visible;"
+         d="M 39.507004,41.577690 C 39.028332,39.304503 40.904334,36.766268 43.091057,36.789315 C 43.091057,36.789315 32.330690,27.531204 32.330690,27.531204 C 29.385899,27.474498 28.061188,29.803820 28.553876,32.131126 L 39.507004,41.577690 z "
+         id="path4438"
+         sodipodi:nodetypes="ccccc" />
+      <path
+         sodipodi:type="arc"
+         style="opacity:1.0000000;color:#000000;fill:none;fill-opacity:1.0000000;fill-rule:evenodd;stroke:url(#linearGradient2372);stroke-width:0.80273360;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10.000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;visibility:visible;display:inline;overflow:visible;"
+         id="path4450"
+         sodipodi:cx="17.500893"
+         sodipodi:cy="18.920233"
+         sodipodi:rx="11.048544"
+         sodipodi:ry="11.048544"
+         d="M 28.549437 18.920233 A 11.048544 11.048544 0 1 1  6.4523487,18.920233 A 11.048544 11.048544 0 1 1  28.549437 18.920233 z"
+         transform="matrix(1.245743,0.000000,0.000000,1.245743,-3.425346,-6.177033)" />
+      <path
+         transform="matrix(0.497764,0.000000,0.000000,0.609621,8.973526,15.61929)"
+         d="M 40.658640 37.967922 A 16.528622 3.9332814 0 1 1  7.6013966,37.967922 A 16.528622 3.9332814 0 1 1  40.658640 37.967922 z"
+         sodipodi:ry="3.9332814"
+         sodipodi:rx="16.528622"
+         sodipodi:cy="37.967922"
+         sodipodi:cx="24.130018"
+         id="path4485"
+         style="opacity:1.0000000;color:#000000;fill:url(#radialGradient4493);fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10.000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;visibility:visible;display:inline;overflow:visible;"
+         sodipodi:type="arc" />
+      <rect
+         style="opacity:0.43315509;color:#000000;fill:none;fill-opacity:1.0000000;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.0000311;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10.000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;visibility:visible;display:inline;overflow:visible;"
+         id="rect4495"
+         width="19.048439"
+         height="4.4404783"
+         x="40.373337"
+         y="0.14086054"
+         rx="2.1366608"
+         ry="1.8879365"
+         transform="matrix(0.752986,0.658037,-0.648902,0.760872,0.000000,0.000000)" />
+      <path
+         sodipodi:type="arc"
+         style="color:#000000;fill:url(#radialGradient4460);fill-opacity:1.0000000;fill-rule:evenodd;stroke:#3063a3;stroke-width:0.71499395;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10.000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;"
+         id="path4452"
+         sodipodi:cx="17.589281"
+         sodipodi:cy="18.478292"
+         sodipodi:rx="8.3085051"
+         sodipodi:ry="8.3085051"
+         d="M 25.897786 18.478292 A 8.3085051 8.3085051 0 1 1  9.2807760,18.478292 A 8.3085051 8.3085051 0 1 1  25.897786 18.478292 z"
+         transform="matrix(1.398614,0.000000,0.000000,1.398614,-6.224338,-8.298958)" />
+      <path
+         style="opacity:0.83422458;color:#000000;fill:url(#radialGradient4473);fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10.000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;visibility:visible;display:inline;overflow:visible;"
+         d="M 18.156915,7.3966938 C 12.949325,7.3966938 8.7323681,11.613651 8.7323681,16.821241 C 8.7323681,18.325216 9.1526753,19.709014 9.7795400,20.971144 C 11.031920,21.432757 12.362297,21.746827 13.774307,21.746827 C 19.945262,21.746827 24.873589,16.885190 25.254413,10.809698 C 23.523449,8.7641668 21.044374,7.3966938 18.156915,7.3966938 z "
+         id="path4462" />
+    </g>
+  </g>
+</svg>
diff --git a/l10n/de.ts b/l10n/de.ts
new file mode 100644 (file)
index 0000000..94c0901
--- /dev/null
@@ -0,0 +1,1598 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="de_DE">
+<context>
+    <name>Application</name>
+    <message>
+        <location filename="../pv/application.cpp" line="121"/>
+        <source>Some parts of the application may still use the previous language. Re-opening the affected windows or restarting the application will remedy this.</source>
+        <translation>Einige Teile der Anwendung verwenden vielleicht noch die vorherige Sprache. Sollte das der Fall sein, kann dies durch ein Schließen und neu Ã–ffnen der betroffenen Fenster oder der Anwendung behoben werden.</translation>
+    </message>
+</context>
+<context>
+    <name>QApplication</name>
+    <message>
+        <source>Select a decoder to see its description here.</source>
+        <translation type="vanished">Wähle einen Dekoder, um dessen Beschreibung hier lesen zu können.</translation>
+    </message>
+    <message>
+        <source>Session %1</source>
+        <translation type="obsolete">Analysesitzung %1</translation>
+    </message>
+    <message>
+        <location filename="../pv/devices/device.cpp" line="70"/>
+        <source>Querying config key %1 is not allowed</source>
+        <translatorcomment>Internal message</translatorcomment>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../pv/devices/device.cpp" line="79"/>
+        <source>Querying config key %1 resulted in %2</source>
+        <translatorcomment>Internal message</translatorcomment>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../pv/devices/device.cpp" line="93"/>
+        <source>Unknown type supplied when attempting to query %1</source>
+        <translatorcomment>Internal message</translatorcomment>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../pv/devicemanager.cpp" line="274"/>
+        <source>Error when scanning device driver &apos;%1&apos;: %2</source>
+        <translatorcomment>Internal message</translatorcomment>
+        <translation></translation>
+    </message>
+</context>
+<context>
+    <name>QHexView</name>
+    <message>
+        <location filename="../pv/views/decoder_binary/QHexView.cpp" line="288"/>
+        <source>No data available</source>
+        <translation>Keine Daten vorhanden</translation>
+    </message>
+</context>
+<context>
+    <name>QObject</name>
+    <message>
+        <location filename="../pv/devicemanager.cpp" line="65"/>
+        <source>Cancel</source>
+        <translation>Abbrechen</translation>
+    </message>
+    <message>
+        <location filename="../pv/devicemanager.cpp" line="96"/>
+        <source>Scanning for devices that driver %1 can access...</source>
+        <translation>Suche nach Geräten, die von Treiber %1 angesprochen werden können...</translation>
+    </message>
+    <message>
+        <location filename="../main.cpp" line="113"/>
+        <source>Stack trace of previous crash:</source>
+        <translatorcomment>Internal message</translatorcomment>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../main.cpp" line="127"/>
+        <source>Don&apos;t show this message again</source>
+        <translation>Diese Meldung in Zukunft nicht mehr anzeigen</translation>
+    </message>
+    <message>
+        <location filename="../main.cpp" line="130"/>
+        <source>When %1 last crashed, it created a stack trace.
+A human-readable form has been saved to disk and was written to the log. You may access it from the settings dialog.</source>
+        <translatorcomment>Internal message</translatorcomment>
+        <translation></translation>
+    </message>
+</context>
+<context>
+    <name>SubWindow</name>
+    <message>
+        <source>&lt;p align=&apos;right&apos;&gt;Tags: %1&lt;/p&gt;</source>
+        <translation type="vanished">&lt;p align=&apos;right&apos;&gt;Stichworte: %1&lt;/p&gt;</translation>
+    </message>
+</context>
+<context>
+    <name>pv::MainWindow</name>
+    <message>
+        <location filename="../pv/mainwindow.cpp" line="69"/>
+        <source>PulseView</source>
+        <translatorcomment>Name</translatorcomment>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../pv/mainwindow.cpp" line="276"/>
+        <source>Decoder Selector</source>
+        <translation>Protokolldekoder</translation>
+    </message>
+    <message>
+        <location filename="../pv/mainwindow.cpp" line="329"/>
+        <source>Session %1</source>
+        <translation>Analysesitzung %1</translation>
+    </message>
+    <message>
+        <location filename="../pv/mainwindow.cpp" line="511"/>
+        <source>Create New Session</source>
+        <translation>Neue Analysesitzung</translation>
+    </message>
+    <message>
+        <location filename="../pv/mainwindow.cpp" line="517"/>
+        <source>Start/Stop Acquisition</source>
+        <translation>Datenerfassung starten/stoppen</translation>
+    </message>
+    <message>
+        <location filename="../pv/mainwindow.cpp" line="525"/>
+        <source>Settings</source>
+        <translation>Einstellungen</translation>
+    </message>
+    <message>
+        <location filename="../pv/mainwindow.cpp" line="577"/>
+        <source>Reload</source>
+        <translation>Neu laden</translation>
+    </message>
+    <message>
+        <location filename="../pv/mainwindow.cpp" line="632"/>
+        <location filename="../pv/mainwindow.cpp" line="826"/>
+        <location filename="../pv/mainwindow.cpp" line="852"/>
+        <source>Confirmation</source>
+        <translation>Bestätigung</translation>
+    </message>
+    <message>
+        <location filename="../pv/mainwindow.cpp" line="633"/>
+        <source>There is unsaved data. Close anyway?</source>
+        <translation>Es gibt noch ungespeicherte Daten. Trotzdem beenden?</translation>
+    </message>
+    <message>
+        <location filename="../pv/mainwindow.cpp" line="577"/>
+        <location filename="../pv/mainwindow.cpp" line="580"/>
+        <source>Run</source>
+        <translation>Starten</translation>
+    </message>
+    <message>
+        <location filename="../pv/mainwindow.cpp" line="586"/>
+        <source>Stop</source>
+        <translation>Stoppen</translation>
+    </message>
+    <message>
+        <location filename="../pv/mainwindow.cpp" line="827"/>
+        <location filename="../pv/mainwindow.cpp" line="853"/>
+        <source>This session contains unsaved data. Close it anyway?</source>
+        <translation>Die Daten dieser Analysesitzung wurden nicht gespeichert. Trotzdem schließen?</translation>
+    </message>
+</context>
+<context>
+    <name>pv::Session</name>
+    <message>
+        <location filename="../pv/session.cpp" line="481"/>
+        <source>Failed to select device</source>
+        <translation>Fehler beim Auswählen des Gerätes</translation>
+    </message>
+    <message>
+        <location filename="../pv/session.cpp" line="530"/>
+        <source>Failed to open device</source>
+        <translation>Fehler beim Ã–ffnen des Gerätes</translation>
+    </message>
+    <message>
+        <location filename="../pv/session.cpp" line="632"/>
+        <source>Error</source>
+        <translation>Fehler</translation>
+    </message>
+    <message>
+        <location filename="../pv/session.cpp" line="633"/>
+        <source>Unexpected input format: %s</source>
+        <translation>Unerwartetes Importformat: %s</translation>
+    </message>
+    <message>
+        <location filename="../pv/session.cpp" line="668"/>
+        <source>Failed to load %1</source>
+        <translation>Fehler beim Laden von %1</translation>
+    </message>
+    <message>
+        <location filename="../pv/session.cpp" line="703"/>
+        <source>No active device set, can&apos;t start acquisition.</source>
+        <translation>Kein Gerät aktiv, kann Datenerfassung nicht starten.</translation>
+    </message>
+    <message>
+        <location filename="../pv/session.cpp" line="716"/>
+        <source>No channels enabled.</source>
+        <translation>Keine aktiven Kanäle vorhanden.</translation>
+    </message>
+    <message>
+        <location filename="../pv/session.cpp" line="1171"/>
+        <source>Out of memory, acquisition stopped.</source>
+        <translation>Nicht genügend Arbeitsspeicher vorhanden, Datenerfassung wurde gestoppt.</translation>
+    </message>
+    <message>
+        <location filename="../pv/session.cpp" line="1377"/>
+        <source>Can&apos;t handle more than 64 logic channels.</source>
+        <translatorcomment>Internal message</translatorcomment>
+        <translation></translation>
+    </message>
+</context>
+<context>
+    <name>pv::StoreSession</name>
+    <message>
+        <location filename="../pv/storesession.cpp" line="114"/>
+        <source>Can&apos;t save logic channel without data.</source>
+        <translation>Kann Logikkanal nicht speichern, da er keine Daten beinhaltet.</translation>
+    </message>
+    <message>
+        <location filename="../pv/storesession.cpp" line="130"/>
+        <source>Can&apos;t save analog channel without data.</source>
+        <translation>Kann Analogkanal nicht speichern, da er keine Daten beinhaltet.</translation>
+    </message>
+    <message>
+        <location filename="../pv/storesession.cpp" line="142"/>
+        <source>No channels enabled.</source>
+        <translation>Keine Kanäle aktiviert.</translation>
+    </message>
+    <message>
+        <location filename="../pv/storesession.cpp" line="167"/>
+        <source>Can&apos;t save range without sample data.</source>
+        <translation>In dem gewählten Bereich befinden sich keine Daten zum Speichern.</translation>
+    </message>
+    <message>
+        <location filename="../pv/storesession.cpp" line="188"/>
+        <location filename="../pv/storesession.cpp" line="295"/>
+        <source>Error while saving: </source>
+        <translation>Fehler beim Speichern: </translation>
+    </message>
+</context>
+<context>
+    <name>pv::data::DecodeSignal</name>
+    <message>
+        <location filename="../pv/data/decodesignal.cpp" line="194"/>
+        <source>No decoders</source>
+        <translation>Keine Protokolldekoder</translation>
+    </message>
+    <message>
+        <location filename="../pv/data/decodesignal.cpp" line="201"/>
+        <source>There are no channels assigned to this decoder</source>
+        <translation>Dem Protokolldekoder sind keine Kanäle zugeordnet</translation>
+    </message>
+    <message>
+        <location filename="../pv/data/decodesignal.cpp" line="215"/>
+        <source>One or more required channels have not been specified</source>
+        <translation>Mindestens ein notwendiger Kanal wurde noch nicht zugeordnet</translation>
+    </message>
+    <message>
+        <location filename="../pv/data/decodesignal.cpp" line="234"/>
+        <source>No input data</source>
+        <translation>Keine Daten zum Auswerten vorhanden</translation>
+    </message>
+    <message>
+        <location filename="../pv/data/decodesignal.cpp" line="1160"/>
+        <source>Decoder reported an error</source>
+        <translation>Protokolldekoder meldet Fehler</translation>
+    </message>
+    <message>
+        <location filename="../pv/data/decodesignal.cpp" line="1296"/>
+        <source>Failed to create decoder instance</source>
+        <translation>Fehler beim Erzeugen des Protokolldekoders</translation>
+    </message>
+</context>
+<context>
+    <name>pv::data::SignalBase</name>
+    <message>
+        <location filename="../pv/data/signalbase.cpp" line="409"/>
+        <source>Signal average</source>
+        <translation>Durchschnittlicher Signalpegel</translation>
+    </message>
+    <message>
+        <location filename="../pv/data/signalbase.cpp" line="410"/>
+        <source>0.9V (for 1.8V CMOS)</source>
+        <translation>0.9V (für 1.8V CMOS)</translation>
+    </message>
+    <message>
+        <location filename="../pv/data/signalbase.cpp" line="411"/>
+        <source>1.8V (for 3.3V CMOS)</source>
+        <translation>1.8V (für 3.3V CMOS)</translation>
+    </message>
+    <message>
+        <location filename="../pv/data/signalbase.cpp" line="412"/>
+        <source>2.5V (for 5.0V CMOS)</source>
+        <translation>2.5V (für 5.0V CMOS)</translation>
+    </message>
+    <message>
+        <location filename="../pv/data/signalbase.cpp" line="413"/>
+        <source>1.5V (for TTL)</source>
+        <translation>1.5V (für TTL)</translation>
+    </message>
+    <message>
+        <location filename="../pv/data/signalbase.cpp" line="418"/>
+        <source>Signal average +/- 15%</source>
+        <translation>Durchschnittlicher Signalpegel +/- 15%</translation>
+    </message>
+    <message>
+        <location filename="../pv/data/signalbase.cpp" line="419"/>
+        <source>0.3V/1.2V (for 1.8V CMOS)</source>
+        <translation>0.3V/1.2V (für 1.8V CMOS)</translation>
+    </message>
+    <message>
+        <location filename="../pv/data/signalbase.cpp" line="420"/>
+        <source>0.7V/2.5V (for 3.3V CMOS)</source>
+        <translation>0.7V/2.5V (für 3.3V CMOS)</translation>
+    </message>
+    <message>
+        <location filename="../pv/data/signalbase.cpp" line="421"/>
+        <source>1.3V/3.7V (for 5.0V CMOS)</source>
+        <translation>1.3V/3.7V (für 5.0V CMOS)</translation>
+    </message>
+    <message>
+        <location filename="../pv/data/signalbase.cpp" line="422"/>
+        <source>0.8V/2.0V (for TTL)</source>
+        <translation>0.8V/2.0V (für TTL)</translation>
+    </message>
+</context>
+<context>
+    <name>pv::dialogs::Connect</name>
+    <message>
+        <location filename="../pv/dialogs/connect.cpp" line="58"/>
+        <source>&amp;Scan for devices using driver above</source>
+        <translation>Nach Geräten &amp;suchen, die der ausgewählte Treiber ansprechen kann</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/connect.cpp" line="63"/>
+        <source>Connect to Device</source>
+        <translation>Mit Gerät verbinden</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/connect.cpp" line="75"/>
+        <source>Step 1: Choose the driver</source>
+        <translation>Schritt 1: Treiber auswählen</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/connect.cpp" line="79"/>
+        <source>&amp;USB</source>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/connect.cpp" line="80"/>
+        <source>Serial &amp;Port</source>
+        <translation>Serielle Sch&amp;nittstelle</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/connect.cpp" line="81"/>
+        <source>&amp;TCP/IP</source>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/connect.cpp" line="116"/>
+        <source>Protocol:</source>
+        <translation>Protokoll:</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/connect.cpp" line="134"/>
+        <source>Step 2: Choose the interface</source>
+        <translation>Schritt 2: Schnittstelle auswählen</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/connect.cpp" line="140"/>
+        <source>Step 3: Scan for devices</source>
+        <translation>Schritt 3: Nach Geräten suchen</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/connect.cpp" line="146"/>
+        <source>Step 4: Select the device</source>
+        <translation>Schritt 4: Gerät auswählen</translation>
+    </message>
+</context>
+<context>
+    <name>pv::dialogs::Settings</name>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="134"/>
+        <location filename="../pv/dialogs/settings.cpp" line="213"/>
+        <source>General</source>
+        <translation>Allgemein</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="143"/>
+        <source>Views</source>
+        <translation>Ansichten</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="153"/>
+        <location filename="../pv/dialogs/settings.cpp" line="397"/>
+        <source>Decoders</source>
+        <translation>Protokolldekoder</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="163"/>
+        <source>About</source>
+        <translation>Programmdetails</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="172"/>
+        <source>Logging</source>
+        <translation>Programminterne Meldungen</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="236"/>
+        <source>User interface language</source>
+        <translation>Sprache der Benutzeroberfläche</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="247"/>
+        <source>User interface theme</source>
+        <translation>Design der Benutzeroberfläche</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="249"/>
+        <source>(You may need to restart PulseView for all UI elements to update)</source>
+        <translation>(Ein Neustart von PulseView kann notwendig sein, damit alle Bedienelemente das neue Design Ã¼bernehmen)</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="255"/>
+        <source>System Default</source>
+        <translation>Standard</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="268"/>
+        <source>Qt widget style</source>
+        <translation>Qt-Anzeigestil</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="270"/>
+        <source>(Dark themes look best with the Fusion style)</source>
+        <translation>(Dunkle Designs sehen mit dem Fusion-Stil am besten aus)</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="277"/>
+        <source>Save session &amp;setup along with .sr file</source>
+        <translation>Analyse&amp;sitzungs-Konfiguration zusammen mit .sr-Dateien speichern</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="291"/>
+        <source>Trace View</source>
+        <translation>Signalansicht</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="299"/>
+        <source>Use colored trace &amp;background</source>
+        <translation>Verwende &amp;farbigen Kanalhintergrund</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="303"/>
+        <source>Constantly perform &amp;zoom-to-fit during acquisition</source>
+        <translation>Ständig den &amp;Zoom anpassen, während Daten aufgezeichnet werden</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="307"/>
+        <source>Perform a zoom-to-&amp;fit when acquisition stops</source>
+        <translation>Den Zoom &amp;anpassen, wenn die Datenerfassung stoppt</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="311"/>
+        <source>Show time zero at the trigger</source>
+        <translation>Den Triggerzeitpunkt automatisch als Nullpunkt festlegen</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="315"/>
+        <source>Always keep &amp;newest samples at the right edge during capture</source>
+        <translation>Die neuesten Datenpunkte während der Aufzeichnung immer am rechten &amp;Rand anzeigen</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="319"/>
+        <source>Show data &amp;sampling points</source>
+        <translation>Daten&amp;punkte visuell hervorheben</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="323"/>
+        <source>Fill high areas of logic signals</source>
+        <translation>High-Pegel von Logiksignalen hervorheben</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="330"/>
+        <source>Color to fill high areas of logic signals with</source>
+        <translation>Farbe für hervorgehobene High-Pegel</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="334"/>
+        <source>Show analog minor grid in addition to div grid</source>
+        <translation>Vertikale Unterteilungen nochmals unterteilen</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="338"/>
+        <source>Highlight mouse cursor using a vertical marker line</source>
+        <translation>Position des Mauscursors durch vertikalen Balken hervorheben</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="342"/>
+        <location filename="../pv/dialogs/settings.cpp" line="368"/>
+        <location filename="../pv/dialogs/settings.cpp" line="377"/>
+        <source> pixels</source>
+        <translation> Pixel</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="347"/>
+        <source>Maximum distance from edges before markers snap to them</source>
+        <translation>Abstand zu Signalflanken, bevor Markierer einrasten</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="354"/>
+        <source>Color to fill cursor area with</source>
+        <translation>Farbe für die Auswahl-Markierung</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="357"/>
+        <source>None</source>
+        <translation>Keine</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="358"/>
+        <source>Background</source>
+        <translation>Hintergrundfarbe</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="359"/>
+        <source>Dots</source>
+        <translation>Farbige Abtastpunkte</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="364"/>
+        <source>Conversion threshold display mode (analog traces only)</source>
+        <translation>Darstellung von Konvertierungsschwellen (nur für analoge Kanäle)</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="373"/>
+        <source>Default analog trace div height</source>
+        <translation>Standardgröße von analogen Kanälen</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="382"/>
+        <source>Default logic trace height</source>
+        <translation>Standardgröße von Logikkanälen</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="405"/>
+        <source>Allow configuration of &amp;initial signal state</source>
+        <translation>&amp;Initialzustände von Signalen konfigurierbar machen</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="409"/>
+        <source>Always show all &amp;rows, even if no annotation is visible</source>
+        <translation>Immer alle &amp;Reihen anzeigen, auch wenn hierfür keine dekodierten Werte vorliegen</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="417"/>
+        <source>Annotation export format</source>
+        <translation>Format für zu exportierende Dekodierwerte</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="418"/>
+        <source>%s = sample range; %d: decoder name; %r: row name; %c: class name</source>
+        <translation>%s = Start-/Endsample; %d: Dekodername; %r: Name der Reihe; %c: Klassenname</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="421"/>
+        <source>%1: longest annotation text; %a: all annotation texts; %q: use quotation marks</source>
+        <translation>%1: Längste Beschreibung des dekodierten Wertes; %a: Alle Beschreibungen des dekodierten Wertes; %q: Benutze Anführungszeichen</translation>
+    </message>
+    <message>
+        <source>%s = sample range; %d: decoder name; %r: row name; %q: use quotation marks</source>
+        <translation type="vanished">%s = Start-/Endsample; %d: Dekodername; %c Name der Kategorie; %q: Benutze Anführungszeichen</translation>
+    </message>
+    <message>
+        <source>%s = sample range; %d: decoder name; %c: row name; %q: use quotations marks</source>
+        <translation type="vanished">%s = Start-/Endsample; %d: Dekodername; %c Name der Kategorie; %q: Benutze Anführungszeichen</translation>
+    </message>
+    <message>
+        <source>%1: longest annotation text; %a: all annotation texts</source>
+        <translation type="vanished">%1: Längste Beschreibung des dekodierten Wertes; %a: Alle Beschreibungen des dekodierten Wertes</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="441"/>
+        <source>%1&lt;br /&gt;&lt;a href=&quot;http://%2&quot;&gt;%2&lt;/a&gt;</source>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="442"/>
+        <source>GNU GPL, version 3 or later</source>
+        <translation>GNU GPL, Version 3 oder neuer</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="453"/>
+        <source>Versions, libraries and features:</source>
+        <translation>Versionen, Bibliotheken und Features:</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="460"/>
+        <source>Firmware search paths:</source>
+        <translation>Suchpfade für Firmware:</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="467"/>
+        <source>Protocol decoder search paths:</source>
+        <translation>Suchpfade für Protokolldekoder:</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="474"/>
+        <source>Supported hardware drivers:</source>
+        <translation>Unterstützte Hardwaretreiber:</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="481"/>
+        <source>Supported input formats:</source>
+        <translation>Unterstützte Importformate:</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="488"/>
+        <source>Supported output formats:</source>
+        <translation>Unterstützte Exportformate:</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="496"/>
+        <source>Supported protocol decoders:</source>
+        <translation>Unterstützte Protokolldekoder:</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="537"/>
+        <source>Log level:</source>
+        <translation>Log-Level:</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="542"/>
+        <source> lines</source>
+        <translation> Zeilen</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="551"/>
+        <source>Length of background buffer:</source>
+        <translation>Länge des Logpuffers:</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="557"/>
+        <source>&amp;Save to File</source>
+        <translation>&amp;Speichern</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="564"/>
+        <source>&amp;Pop out</source>
+        <translation>&amp;Abdocken</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="633"/>
+        <source>You selected a dark theme.
+Should I set the user-adjustable colors to better suit your choice?
+
+Please keep in mind that PulseView may need a restart to display correctly.</source>
+        <translation>Es wurde ein dunkles Design gewählt.
+Sollen die benutzerspezifischen Farben entsprechend angepasst werden, damit sie besser harmonieren?
+
+Bei einer Ã„nderung benötigt PulseView eventuell einen Neustart, damit alles korrekt angezeigt wird.</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="639"/>
+        <source>You selected a bright theme.
+Should I set the user-adjustable colors to better suit your choice?
+
+Please keep in mind that PulseView may need a restart to display correctly.</source>
+        <translation>Es wurde ein helles Design gewählt.
+Sollen die benutzerspezifischen Farben entsprechend angepasst werden, damit sie besser harmonieren?
+
+Bei einer Ã„nderung benötigt PulseView eventuell einen Neustart, damit alles korrekt angezeigt wird.</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="792"/>
+        <source>Save Log</source>
+        <translation>Log speichern</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="792"/>
+        <source>Log Files (*.txt *.log);;All Files (*)</source>
+        <translation>Logdateien (*.txt *.log);;Alle Dateien (*)</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="804"/>
+        <source>Success</source>
+        <translation>Erfolg</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="804"/>
+        <source>Log saved to %1.</source>
+        <translation>Log als %1 gespeichert.</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="814"/>
+        <source>Error</source>
+        <translation>Fehler</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="814"/>
+        <source>File %1 could not be written to.</source>
+        <translation>Konnte Datei %1 nicht speichern.</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/settings.cpp" line="828"/>
+        <source>%1 Log</source>
+        <translation></translation>
+    </message>
+</context>
+<context>
+    <name>pv::dialogs::StoreProgress</name>
+    <message>
+        <location filename="../pv/dialogs/storeprogress.cpp" line="44"/>
+        <source>Saving...</source>
+        <translation>Speichere...</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/storeprogress.cpp" line="44"/>
+        <source>Cancel</source>
+        <translation>Abbrechen</translation>
+    </message>
+    <message>
+        <location filename="../pv/dialogs/storeprogress.cpp" line="85"/>
+        <source>Failed to save session.</source>
+        <translation>Beim Speichern trat ein Fehler auf.</translation>
+    </message>
+</context>
+<context>
+    <name>pv::popups::Channels</name>
+    <message>
+        <location filename="../pv/popups/channels.cpp" line="62"/>
+        <location filename="../pv/popups/channels.cpp" line="63"/>
+        <location filename="../pv/popups/channels.cpp" line="273"/>
+        <location filename="../pv/popups/channels.cpp" line="300"/>
+        <source>All</source>
+        <translation>Alle</translation>
+    </message>
+    <message>
+        <location filename="../pv/popups/channels.cpp" line="64"/>
+        <location filename="../pv/popups/channels.cpp" line="65"/>
+        <source>Logic</source>
+        <translation>Logik</translation>
+    </message>
+    <message>
+        <location filename="../pv/popups/channels.cpp" line="66"/>
+        <location filename="../pv/popups/channels.cpp" line="67"/>
+        <source>Analog</source>
+        <translation>Analog</translation>
+    </message>
+    <message>
+        <location filename="../pv/popups/channels.cpp" line="68"/>
+        <source>Named</source>
+        <translation>Benamte</translation>
+    </message>
+    <message>
+        <location filename="../pv/popups/channels.cpp" line="69"/>
+        <source>Unnamed</source>
+        <translation>Unbenamte</translation>
+    </message>
+    <message>
+        <location filename="../pv/popups/channels.cpp" line="70"/>
+        <source>Changing</source>
+        <translation>Sich Ã¤ndernde</translation>
+    </message>
+    <message>
+        <location filename="../pv/popups/channels.cpp" line="71"/>
+        <source>Non-changing</source>
+        <translation>Konstante</translation>
+    </message>
+    <message>
+        <location filename="../pv/popups/channels.cpp" line="141"/>
+        <source>Disable: </source>
+        <translation>Deaktivieren: </translation>
+    </message>
+    <message>
+        <location filename="../pv/popups/channels.cpp" line="149"/>
+        <source>Enable: </source>
+        <translation>Aktivieren: </translation>
+    </message>
+    <message>
+        <location filename="../pv/popups/channels.cpp" line="281"/>
+        <location filename="../pv/popups/channels.cpp" line="301"/>
+        <source>None</source>
+        <translation>Keine</translation>
+    </message>
+</context>
+<context>
+    <name>pv::prop::Bool</name>
+    <message>
+        <location filename="../pv/prop/bool.cpp" line="51"/>
+        <location filename="../pv/prop/bool.cpp" line="82"/>
+        <source>Querying config key %1 resulted in %2</source>
+        <translatorcomment>Internal message</translatorcomment>
+        <translation></translation>
+    </message>
+</context>
+<context>
+    <name>pv::prop::Double</name>
+    <message>
+        <location filename="../pv/prop/double.cpp" line="65"/>
+        <location filename="../pv/prop/double.cpp" line="96"/>
+        <source>Querying config key %1 resulted in %2</source>
+        <translatorcomment>Internal message</translatorcomment>
+        <translation></translation>
+    </message>
+</context>
+<context>
+    <name>pv::prop::Enum</name>
+    <message>
+        <location filename="../pv/prop/enum.cpp" line="113"/>
+        <location filename="../pv/prop/enum.cpp" line="176"/>
+        <source>Querying config key %1 resulted in %2</source>
+        <translatorcomment>Internal message</translatorcomment>
+        <translation></translation>
+    </message>
+</context>
+<context>
+    <name>pv::prop::Int</name>
+    <message>
+        <location filename="../pv/prop/int.cpp" line="65"/>
+        <location filename="../pv/prop/int.cpp" line="128"/>
+        <source>Querying config key %1 resulted in %2</source>
+        <translatorcomment>Internal message</translatorcomment>
+        <translation></translation>
+    </message>
+</context>
+<context>
+    <name>pv::prop::String</name>
+    <message>
+        <location filename="../pv/prop/string.cpp" line="59"/>
+        <location filename="../pv/prop/string.cpp" line="84"/>
+        <source>Querying config key %1 resulted in %2</source>
+        <translatorcomment>Internal message</translatorcomment>
+        <translation></translation>
+    </message>
+</context>
+<context>
+    <name>pv::subwindows::decoder_selector::DecoderCollectionModel</name>
+    <message>
+        <location filename="../pv/subwindows/decoder_selector/model.cpp" line="40"/>
+        <source>Decoder</source>
+        <translation>Dekoder</translation>
+    </message>
+    <message>
+        <location filename="../pv/subwindows/decoder_selector/model.cpp" line="41"/>
+        <source>Name</source>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../pv/subwindows/decoder_selector/model.cpp" line="42"/>
+        <source>ID</source>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../pv/subwindows/decoder_selector/model.cpp" line="49"/>
+        <source>All Decoders</source>
+        <translation>Alle Dekoder</translation>
+    </message>
+</context>
+<context>
+    <name>pv::subwindows::decoder_selector::SubWindow</name>
+    <message>
+        <location filename="../pv/subwindows/decoder_selector/subwindow.cpp" line="49"/>
+        <source>Select a decoder to see its description here.</source>
+        <translation>Wähle einen Dekoder, um dessen Beschreibung hier lesen zu können.</translation>
+    </message>
+    <message>
+        <location filename="../pv/subwindows/decoder_selector/subwindow.cpp" line="247"/>
+        <source>, %1</source>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../pv/subwindows/decoder_selector/subwindow.cpp" line="264"/>
+        <source>&lt;p align=&apos;right&apos;&gt;Tags: %1&lt;/p&gt;</source>
+        <translation>&lt;p align=&apos;right&apos;&gt;Stichworte: %1&lt;/p&gt;</translation>
+    </message>
+    <message>
+        <location filename="../pv/subwindows/decoder_selector/subwindow.cpp" line="311"/>
+        <source>Protocol decoder &lt;b&gt;%1&lt;/b&gt; requires input type &lt;b&gt;%2&lt;/b&gt; which several decoders provide.&lt;br&gt;Choose which one to use:&lt;br&gt;</source>
+        <translation>Protokolldekoder &lt;b&gt;%1&lt;/b&gt; benötigt Daten vom Typ &lt;b&gt;%2&lt;/b&gt;, die von verschiedenen Protokolldekodern bereitgestellt werden. &lt;br&gt;Wähle, welcher benutzt werden soll:&lt;br&gt;</translation>
+    </message>
+    <message>
+        <location filename="../pv/subwindows/decoder_selector/subwindow.cpp" line="319"/>
+        <source>Choose Decoder</source>
+        <translation>Wähle Protokolldekoder</translation>
+    </message>
+</context>
+<context>
+    <name>pv::toolbars::MainBar</name>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="120"/>
+        <source>New &amp;View</source>
+        <translation>Neue &amp;Ansicht</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="126"/>
+        <source>&amp;Open...</source>
+        <translation>&amp;Öffnen...</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="133"/>
+        <source>Restore Session Setu&amp;p...</source>
+        <translation>&amp;Konfiguration der Analysesitzung laden...</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="137"/>
+        <source>&amp;Save As...</source>
+        <translation>&amp;Speichern als...</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="144"/>
+        <source>Save Selected &amp;Range As...</source>
+        <translation>Ausgewählten &amp;Bereich speichern als...</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="151"/>
+        <source>Save Session Setu&amp;p...</source>
+        <translation>&amp;Konfiguration der Analysesitzung speichern...</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="157"/>
+        <source>&amp;Export</source>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="163"/>
+        <source>&amp;Import</source>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="167"/>
+        <source>&amp;Connect to Device...</source>
+        <translation>Mit Gerät &amp;verbinden...</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="228"/>
+        <source>Add protocol decoder</source>
+        <translation>Protokolldekoder hinzufügen</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="244"/>
+        <source>Configure Device</source>
+        <translation>Gerät konfigurieren</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="248"/>
+        <source>Configure Channels</source>
+        <translation>Kanäle konfigurieren</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="357"/>
+        <source>Failed to get sample rate list:</source>
+        <translation>Konnte Liste unterstützter Abtastraten nicht abfragen:</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="420"/>
+        <source>Failed to get sample rate:</source>
+        <translation>Konnte Abtastrate nicht abfragen:</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="461"/>
+        <source>Failed to get sample limit list:</source>
+        <translation>Konnte Liste der maximal erlaubten Abtastraten nicht abfragen:</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="551"/>
+        <source>Failed to configure samplerate:</source>
+        <translation>Konnte Abtastrate nicht einstellen:</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="578"/>
+        <source>Failed to configure sample count:</source>
+        <translation>Konnte Anzahl der Abtastpunkte nicht einstellen:</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="616"/>
+        <source>Missing Cursors</source>
+        <translation>Fehlende Auswahl</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="616"/>
+        <source>You need to set the cursors before you can save the data enclosed by them to a session file (e.g. using the Show Cursors button).</source>
+        <translation>Du musst die Auswahl-Markierer setzen, bevor du die darin befindlichen Daten abspeichern kannst. Verwende hierzu bspw. den Knopf für die Auswahl-Markierer.</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="634"/>
+        <source>Invalid Range</source>
+        <translation>Auswahl ungültig</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="634"/>
+        <source>The cursors don&apos;t define a valid range of samples.</source>
+        <translation>Die Auswahl-Markierer geben keinen gültigen Datenbereich an.</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="646"/>
+        <source>%1 files </source>
+        <translation>%1-Dateien </translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="654"/>
+        <location filename="../pv/toolbars/mainbar.cpp" line="699"/>
+        <source>All Files</source>
+        <translation>Alle Dateien</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="658"/>
+        <location filename="../pv/toolbars/mainbar.cpp" line="817"/>
+        <source>Save File</source>
+        <translation>Speichern</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="670"/>
+        <source>Export %1</source>
+        <translation>%1 exportieren</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="696"/>
+        <source>%1 files</source>
+        <translation>%1-Dateien</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="707"/>
+        <source>Import File</source>
+        <translation>Dateiimport</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="716"/>
+        <source>Import %1</source>
+        <translation>%1 importieren</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="789"/>
+        <location filename="../pv/toolbars/mainbar.cpp" line="834"/>
+        <source>Open File</source>
+        <translation>Öffnen</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="789"/>
+        <source>sigrok Sessions (*.sr);;All Files (*)</source>
+        <translation>sigrok-Datenformat (*.sr);;Alle Dateien (*)</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="817"/>
+        <location filename="../pv/toolbars/mainbar.cpp" line="834"/>
+        <source>PulseView Session Setups (*.pvs);;All Files (*)</source>
+        <translation>Analysesitzungs-Konfigurationen (*.pvs);;Alle Dateien (*)</translation>
+    </message>
+    <message>
+        <location filename="../pv/toolbars/mainbar.cpp" line="895"/>
+        <source>Total sampling time: %1</source>
+        <translatorcomment>Internal message</translatorcomment>
+        <translation></translation>
+    </message>
+</context>
+<context>
+    <name>pv::views::decoder_binary::View</name>
+    <message>
+        <location filename="../pv/views/decoder_binary/view.cpp" line="83"/>
+        <source>Decoder:</source>
+        <translation>Dekoder:</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/decoder_binary/view.cpp" line="87"/>
+        <source>Show data as</source>
+        <translation>Zeige Daten als</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/decoder_binary/view.cpp" line="93"/>
+        <source>Hexdump</source>
+        <translation>Hex-Dump</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/decoder_binary/view.cpp" line="110"/>
+        <source>&amp;Save...</source>
+        <translation>&amp;Speichern...</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/decoder_binary/view.cpp" line="258"/>
+        <location filename="../pv/views/decoder_binary/view.cpp" line="298"/>
+        <source>Save Binary Data</source>
+        <translation>Binäre Daten speichern</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/decoder_binary/view.cpp" line="258"/>
+        <source>Binary Data Files (*.bin);;All Files (*)</source>
+        <translation>Binärdateien (*.bin);;Alle Dateien (*)</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/decoder_binary/view.cpp" line="277"/>
+        <location filename="../pv/views/decoder_binary/view.cpp" line="329"/>
+        <source>Error</source>
+        <translation>Fehler</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/decoder_binary/view.cpp" line="277"/>
+        <location filename="../pv/views/decoder_binary/view.cpp" line="329"/>
+        <source>File %1 could not be written to.</source>
+        <translation>Konnte Datei %1 nicht speichern.</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/decoder_binary/view.cpp" line="298"/>
+        <source>Hex Dumps (*.txt);;All Files (*)</source>
+        <translation>Hex-Dumps (*.txt);;Alle Dateien (*)</translation>
+    </message>
+</context>
+<context>
+    <name>pv::views::decoder_output::View</name>
+    <message>
+        <source>Decoder:</source>
+        <translation type="vanished">Dekoder:</translation>
+    </message>
+    <message>
+        <source>Show data as</source>
+        <translation type="vanished">Zeige Daten als</translation>
+    </message>
+    <message>
+        <source>Hexdump</source>
+        <translation type="vanished">Hex-Dump</translation>
+    </message>
+    <message>
+        <source>&amp;Save...</source>
+        <translation type="vanished">&amp;Speichern...</translation>
+    </message>
+    <message>
+        <source>Save Binary Data</source>
+        <translation type="vanished">Binäre Daten speichern</translation>
+    </message>
+    <message>
+        <source>Binary Data Files (*.bin);;All Files (*)</source>
+        <translation type="vanished">Binärdateien (*.bin);;Alle Dateien (*)</translation>
+    </message>
+    <message>
+        <source>Error</source>
+        <translation type="vanished">Fehler</translation>
+    </message>
+    <message>
+        <source>File %1 could not be written to.</source>
+        <translation type="vanished">Konnte Datei %1 nicht speichern.</translation>
+    </message>
+    <message>
+        <source>Hex Dumps (*.txt);;All Files (*)</source>
+        <translation type="vanished">Hex-Dumps (*.txt);;Alle Dateien (*)</translation>
+    </message>
+</context>
+<context>
+    <name>pv::views::trace::AnalogSignal</name>
+    <message>
+        <location filename="../pv/views/trace/analogsignal.cpp" line="988"/>
+        <source>Number of pos vertical divs</source>
+        <translation>Anzahl Unterteilungen im Positiven</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/analogsignal.cpp" line="995"/>
+        <source>Number of neg vertical divs</source>
+        <translation>Anzahl Unterteilungen im Negativen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/analogsignal.cpp" line="1000"/>
+        <source> pixels</source>
+        <translation> Pixel</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/analogsignal.cpp" line="1004"/>
+        <source>Div height</source>
+        <translation>Höhe einer Unterteilung</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/analogsignal.cpp" line="1021"/>
+        <source>V/div</source>
+        <translation>V/div</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/analogsignal.cpp" line="1025"/>
+        <source>Vertical resolution</source>
+        <translation>Vertikale Auflösung</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/analogsignal.cpp" line="1034"/>
+        <source>Autoranging</source>
+        <translation>Automatische Skalierung</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/analogsignal.cpp" line="1039"/>
+        <source>none</source>
+        <translation>keine</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/analogsignal.cpp" line="1041"/>
+        <source>to logic via threshold</source>
+        <translation>zu Logik mittels Schwellwert</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/analogsignal.cpp" line="1043"/>
+        <source>to logic via schmitt-trigger</source>
+        <translation>zu Logik mittels Schmitt-Trigger</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/analogsignal.cpp" line="1049"/>
+        <source>Conversion</source>
+        <translation>Konvertierung</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/analogsignal.cpp" line="1058"/>
+        <source>Conversion threshold(s)</source>
+        <translation>Konvertierungs-Schwellwert(e)</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/analogsignal.cpp" line="1068"/>
+        <source>analog</source>
+        <translation>nur analog</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/analogsignal.cpp" line="1069"/>
+        <source>converted</source>
+        <translation>nur konvertiert</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/analogsignal.cpp" line="1070"/>
+        <source>analog+converted</source>
+        <translation>analog+konvertiert</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/analogsignal.cpp" line="1075"/>
+        <source>Show traces for</source>
+        <translation>Anzuzeigende Signale</translation>
+    </message>
+</context>
+<context>
+    <name>pv::views::trace::Cursor</name>
+    <message>
+        <location filename="../pv/views/trace/cursor.cpp" line="97"/>
+        <source>Disable snapping</source>
+        <translation>Einrasten deaktivieren</translation>
+    </message>
+</context>
+<context>
+    <name>pv::views::trace::CursorPair</name>
+    <message>
+        <location filename="../pv/views/trace/cursorpair.cpp" line="128"/>
+        <source>Display interval</source>
+        <translation>Intervall anzeigen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/cursorpair.cpp" line="140"/>
+        <source>Display frequency</source>
+        <translation>Frequenz anzeigen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/cursorpair.cpp" line="152"/>
+        <source>Display samples</source>
+        <translation>Samples anzeigen</translation>
+    </message>
+</context>
+<context>
+    <name>pv::views::trace::DecodeTrace</name>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="448"/>
+        <source>&lt;p&gt;&lt;i&gt;No decoders in the stack&lt;/i&gt;&lt;/p&gt;</source>
+        <translation>&lt;p&gt;&lt;i&gt;Keine Protokolldekoder vorhanden&lt;/i&gt;&lt;/p&gt;</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="459"/>
+        <source>&lt;i&gt;* Required channels&lt;/i&gt;</source>
+        <translation>&lt;i&gt;* Notwendige Kanäle&lt;/i&gt;</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="463"/>
+        <source>Stack Decoder</source>
+        <translation>Protokolldekoder stapeln</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="464"/>
+        <source>Stack a higher-level decoder on top of this one</source>
+        <translation>Weiteren Protokolldekoder auf diesen stapeln</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="478"/>
+        <source>Delete</source>
+        <translation>Löschen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="520"/>
+        <source>Resume decoding</source>
+        <translation>Dekodierung fortsetzen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="527"/>
+        <source>Pause decoding</source>
+        <translation>Dekodierung anhalten</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="535"/>
+        <source>Copy annotation text to clipboard</source>
+        <translation>Dekodierten Wert in die Zwischenablage kopieren</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="544"/>
+        <source>Export all annotations</source>
+        <translation>Alle dekodierten Werte exportieren</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="551"/>
+        <source>Export all annotations for this row</source>
+        <translation>Alle dekodierten Werte dieser Zeile exportieren</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="560"/>
+        <source>Export all annotations, starting here</source>
+        <translation>Alle dekodierten Werte ab hier exportieren</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="567"/>
+        <source>Export annotations for this row, starting here</source>
+        <translation>Alle dekodierten Werte dieser Zeile ab hier exportieren</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="576"/>
+        <source>Export all annotations within cursor range</source>
+        <translation>Alle dekodierten Werte innerhalb des gewählten Bereiches exportieren</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="583"/>
+        <source>Export annotations for this row within cursor range</source>
+        <translation>Alle dekodierten Werte dieser Zeile innerhalb des gewählten Bereiches exportieren</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="1100"/>
+        <source>%1:
+%2</source>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="1144"/>
+        <source>&lt;b&gt;%1&lt;/b&gt; (%2) %3</source>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="1214"/>
+        <source>Export annotations</source>
+        <translation>Dekodierte Werte exportieren</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="1214"/>
+        <source>Text Files (*.txt);;All Files (*)</source>
+        <translation>Textdateien (*.txt);;Alle Dateien (*)</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="1279"/>
+        <source>Error</source>
+        <translation>Fehler</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="1279"/>
+        <source>File %1 could not be written to.</source>
+        <translation>Konnte Datei %1 nicht speichern.</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="1332"/>
+        <source>Show this row</source>
+        <translation>Diese Zeile anzeigen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="1343"/>
+        <source>Show All</source>
+        <translation>Alle anzeigen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/decodetrace.cpp" line="1351"/>
+        <source>Hide All</source>
+        <translation>Alle verstecken</translation>
+    </message>
+</context>
+<context>
+    <name>pv::views::trace::Flag</name>
+    <message>
+        <location filename="../pv/views/trace/flag.cpp" line="132"/>
+        <source>Text</source>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/flag.cpp" line="141"/>
+        <source>Delete</source>
+        <translation>Löschen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/flag.cpp" line="146"/>
+        <source>Disable snapping</source>
+        <translation>Einrasten deaktivieren</translation>
+    </message>
+</context>
+<context>
+    <name>pv::views::trace::Header</name>
+    <message>
+        <location filename="../pv/views/trace/header.cpp" line="137"/>
+        <source>Group</source>
+        <translation>Gruppieren</translation>
+    </message>
+</context>
+<context>
+    <name>pv::views::trace::LogicSignal</name>
+    <message>
+        <location filename="../pv/views/trace/logicsignal.cpp" line="451"/>
+        <source>No trigger</source>
+        <translation>Kein Trigger</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/logicsignal.cpp" line="456"/>
+        <source>Trigger on rising edge</source>
+        <translation>Trigger auf steigende Flanke</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/logicsignal.cpp" line="461"/>
+        <source>Trigger on high level</source>
+        <translation>Trigger auf High-Pegel</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/logicsignal.cpp" line="466"/>
+        <source>Trigger on falling edge</source>
+        <translation>Trigger auf fallende Flanke</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/logicsignal.cpp" line="471"/>
+        <source>Trigger on low level</source>
+        <translation>Trigger auf Low-Pegel</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/logicsignal.cpp" line="476"/>
+        <source>Trigger on rising or falling edge</source>
+        <translation>Trigger auf steigende oder fallende Flanke</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/logicsignal.cpp" line="563"/>
+        <source> pixels</source>
+        <translation> Pixel</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/logicsignal.cpp" line="567"/>
+        <source>Trace height</source>
+        <translation>Kanalgröße</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/logicsignal.cpp" line="591"/>
+        <source>Trigger</source>
+        <translation>Trigger</translation>
+    </message>
+</context>
+<context>
+    <name>pv::views::trace::Ruler</name>
+    <message>
+        <location filename="../pv/views/trace/ruler.cpp" line="153"/>
+        <source>Create marker here</source>
+        <translation>Hier neue Markierung anlegen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/ruler.cpp" line="157"/>
+        <source>Set as zero point</source>
+        <translation>Als Nullpunkt setzen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/ruler.cpp" line="162"/>
+        <source>Reset zero point</source>
+        <translation>Nullpunkt zurücksetzen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/ruler.cpp" line="175"/>
+        <source>Disable mouse hover marker</source>
+        <translation>Mauszeigerbalken deaktivieren</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/ruler.cpp" line="175"/>
+        <source>Enable mouse hover marker</source>
+        <translation>Mauszeigerbalken aktivieren</translation>
+    </message>
+</context>
+<context>
+    <name>pv::views::trace::Signal</name>
+    <message>
+        <location filename="../pv/views/trace/signal.cpp" line="148"/>
+        <source>Name</source>
+        <translation></translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/signal.cpp" line="159"/>
+        <source>Disable</source>
+        <translation>Deaktivieren</translation>
+    </message>
+</context>
+<context>
+    <name>pv::views::trace::StandardBar</name>
+    <message>
+        <location filename="../pv/views/trace/standardbar.cpp" line="54"/>
+        <source>Zoom &amp;In</source>
+        <translation>H&amp;ineinzoomen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/standardbar.cpp" line="62"/>
+        <source>Zoom &amp;Out</source>
+        <translation>Hera&amp;uszoomen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/standardbar.cpp" line="70"/>
+        <source>Zoom to &amp;Fit</source>
+        <translation>&amp;Passend zoomen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/standardbar.cpp" line="82"/>
+        <source>Show &amp;Cursors</source>
+        <translation>&amp;Auswahl-Markierer anzeigen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/standardbar.cpp" line="85"/>
+        <source>Display last segment only</source>
+        <translation>Nur letztes Segment anzeigen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/standardbar.cpp" line="90"/>
+        <source>Display last complete segment only</source>
+        <translation>Nur letztes vollständiges Segment anzeigen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/standardbar.cpp" line="95"/>
+        <source>Display a single segment</source>
+        <translation>Einzelnes Segment anzeigen</translation>
+    </message>
+</context>
+<context>
+    <name>pv::views::trace::TimeMarker</name>
+    <message>
+        <location filename="../pv/views/trace/timemarker.cpp" line="191"/>
+        <source>Time</source>
+        <translation>Zeit</translation>
+    </message>
+</context>
+<context>
+    <name>pv::views::trace::Trace</name>
+    <message>
+        <location filename="../pv/views/trace/trace.cpp" line="206"/>
+        <source>Create marker here</source>
+        <translation>Hier neue Markierung anlegen</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/trace.cpp" line="315"/>
+        <source>Color</source>
+        <translation>Farbe</translation>
+    </message>
+    <message>
+        <location filename="../pv/views/trace/trace.cpp" line="380"/>
+        <source>Name</source>
+        <translation></translation>
+    </message>
+</context>
+<context>
+    <name>pv::views::trace::TraceGroup</name>
+    <message>
+        <location filename="../pv/views/trace/tracegroup.cpp" line="140"/>
+        <source>Ungroup</source>
+        <translation>Trennen</translation>
+    </message>
+</context>
+<context>
+    <name>pv::widgets::DecoderGroupBox</name>
+    <message>
+        <location filename="../pv/widgets/decodergroupbox.cpp" line="48"/>
+        <source>Show/hide this decoder trace</source>
+        <translation>Dekoder anzeigen/verbergen</translation>
+    </message>
+    <message>
+        <location filename="../pv/widgets/decodergroupbox.cpp" line="58"/>
+        <source>Delete this decoder trace</source>
+        <translation>Protokolldekoder entfernen</translation>
+    </message>
+</context>
+<context>
+    <name>pv::widgets::DeviceToolButton</name>
+    <message>
+        <location filename="../pv/widgets/devicetoolbutton.cpp" line="75"/>
+        <location filename="../pv/widgets/devicetoolbutton.cpp" line="82"/>
+        <source>&lt;No Device&gt;</source>
+        <translation>&lt;Kein Gerät&gt;</translation>
+    </message>
+</context>
+<context>
+    <name>pv::widgets::ExportMenu</name>
+    <message>
+        <location filename="../pv/widgets/exportmenu.cpp" line="71"/>
+        <source>Export %1...</source>
+        <translation>%1 exportieren...</translation>
+    </message>
+</context>
+<context>
+    <name>pv::widgets::ImportMenu</name>
+    <message>
+        <location filename="../pv/widgets/importmenu.cpp" line="68"/>
+        <source>Import %1...</source>
+        <translation>%1 importieren...</translation>
+    </message>
+</context>
+</TS>
index a77969ea7f72313e6c42b71cf0cd09980c53e0da..da5fffcdb3af28d4ada2044e32e88c4a2cd7cd3b 100644 (file)
--- a/main.cpp
+++ b/main.cpp
 #include <getopt.h>
 #include <vector>
 
+#ifdef ENABLE_FLOW
+#include <gstreamermm.h>
+#include <libsigrokflow/libsigrokflow.hpp>
+#endif
+
 #include <libsigrokcxx/libsigrokcxx.hpp>
 
 #include <QCheckBox>
@@ -154,6 +159,7 @@ void usage()
                "  -d, --driver                    Specify the device driver to use\n"
                "  -D, --dont-scan                 Don't auto-scan for devices, use -d spec only\n"
                "  -i, --input-file                Load input from file\n"
+               "  -s, --settings                  Load PulseView session setup from file\n"
                "  -I, --input-format              Input format\n"
                "  -c, --clean                     Don't restore previous sessions on startup\n"
                "\n", PV_BIN_NAME);
@@ -163,12 +169,20 @@ int main(int argc, char *argv[])
 {
        int ret = 0;
        shared_ptr<sigrok::Context> context;
-       string open_file_format, driver;
+       string open_file_format, open_setup_file, driver;
        vector<string> open_files;
        bool restore_sessions = true;
        bool do_scan = true;
        bool show_version = false;
 
+#ifdef ENABLE_FLOW
+       // Initialise gstreamermm. Must be called before any other GLib stuff.
+       Gst::init();
+
+       // Initialize libsigrokflow. Must be called after Gst::init().
+       Srf::init();
+#endif
+
        Application a(argc, argv);
 
 #ifdef ANDROID
@@ -186,6 +200,7 @@ int main(int argc, char *argv[])
                        {"driver", required_argument, nullptr, 'd'},
                        {"dont-scan", no_argument, nullptr, 'D'},
                        {"input-file", required_argument, nullptr, 'i'},
+                       {"settings", required_argument, nullptr, 's'},
                        {"input-format", required_argument, nullptr, 'I'},
                        {"clean", no_argument, nullptr, 'c'},
                        {"log-to-stdout", no_argument, nullptr, 's'},
@@ -193,7 +208,7 @@ int main(int argc, char *argv[])
                };
 
                const int c = getopt_long(argc, argv,
-                       "h?VDcl:d:i:I:", long_options, nullptr);
+                       "h?VDcl:d:i:s:I:", long_options, nullptr);
                if (c == -1)
                        break;
 
@@ -240,6 +255,10 @@ int main(int argc, char *argv[])
                        open_files.emplace_back(optarg);
                        break;
 
+               case 's':
+                       open_setup_file = optarg;
+                       break;
+
                case 'I':
                        open_file_format = optarg;
                        break;
@@ -260,8 +279,10 @@ int main(int argc, char *argv[])
 
        // Prepare the global settings since logging needs them early on
        pv::GlobalSettings settings;
+       settings.add_change_handler(&a);  // Only the application object can't register itself
        settings.save_internal_defaults();
        settings.set_defaults_where_needed();
+       settings.apply_language();
        settings.apply_theme();
 
        pv::logging.init();
@@ -321,7 +342,7 @@ int main(int argc, char *argv[])
                                w.add_default_session();
                        else
                                for (string& open_file : open_files)
-                                       w.add_session_with_file(open_file, open_file_format);
+                                       w.add_session_with_file(open_file, open_file_format, open_setup_file);
 
 #ifdef ENABLE_SIGNALS
                        if (SignalHandler::prepare_signals()) {
index 685c9cd89932bed2bccc35dc526cc5f212ba4250..05c50f36b26ad70594287808b667db75567ab7e4 100644 (file)
@@ -15,7 +15,7 @@ program even when you don't have any hardware to use it with.
 
 The device selector offers two methods to choose the device to use. If you click on the small
 arrow on the side, you see a list of devices PulseView has recognized. If the device you want
-to use it listed, you can just select it here to use it.
+to use is listed, you can just select it here.
 
 image::device_selector_dropdown.png[]
 
index 7aa487b488b4416d839fdf8337a07ff901711aa1..b6547fe409f6f9b8eb076c563d4ea7df25641308 100644 (file)
@@ -30,10 +30,12 @@ you'd expect. To do so, you'll want to use cursors and markers.
 
 In the picture above, you can enable the cursor by clicking on the cursor button.
 You can move both of its boundaries around by clicking on the blue flags in the
-time scale area. The area between the two boundary lines shows the time distance
-and its inverse (i.e. the frequency). If you can't see it, just zoom in until it
-shows. You can also move both boundaries at the same time by dragging the label
-where this information is shown.
+time scale area. The area between the two boundary lines shows the time distance,
+its inverse (i.e. the frequency) and/or the number of samples encompassed. If there's
+not enough space to see these, you can either zoom in until it shows, hover the mouse
+cursor over the label in the middle or right-click on the label to  configure what
+you want to see. You can also move both boundaries at the same time by dragging said
+label.
 
 image::pv_cursors_markers.png[]
 
@@ -48,6 +50,19 @@ the ruler or a signal trace.
 You can click on its label and you'll have the option to change its name, or
 drag it to reposition it.
 
+When you have multiple markers, you can have PulseView show you the time difference
+between the markers by hovering over one of them, like so:
+
+image::pv_marker_deltas.png[]
+
+This works on the cursor, too.
+
+Speaking of which - if you want to place or move the cursor ranges quickly, you
+can also press '1' and '2' on your keyboard to attach either side to your mouse
+cursor. They will stay put when you either press Esc or click with the left
+mouse button. This also works when the cursor isn't even showing, so using this
+method allows you to place the cursor quickly without having to enable it first.
+
 [NOTE]
 For timing comparison purposes, you can also enable a vertical marker line that
 follows your mouse cursor: _Settings_ -> _Views_ -> _Highlight mouse cursor_
@@ -55,7 +70,7 @@ follows your mouse cursor: _Settings_ -> _Views_ -> _Highlight mouse cursor_
 [NOTE]
 There is also a special kind of marker that appears for each time the data
 acquisition device has triggered. It cannot be moved and appears as a vertical
-dashed line.
+dashed blue line.
 
 === Special-Purpose Decoders
 
@@ -63,7 +78,7 @@ There are some decoders available that analyze the data instead of decoding it.
 You can make use of them to examine various properties of the signals that are
 of interest to you.
 
-Their names are:
+Among them are:
 
 * Counter - counts pulses and/or groups of pulses (i.e. words)
 * Guess bitrate - guesses the bitrate when using a serial protocol
@@ -72,6 +87,7 @@ Their names are:
 
 === Other Features
 
+==== Signal Label Area Resizing
 Trace Views also allow you to maximize the viewing area by minimizing the area
 occupied by the label area on the left. To do this, simply position the mouse
 cursor at the right edge of the label area (or left edge of the viewing area).
@@ -80,5 +96,20 @@ Your mouse cursor will change shape and you now can drag the border.
 This way, you can give signals long, expressive names without clogging up the
 view area.
 
-Also, you can create multiple views by clicking on the "New View" button on
-the very left of the toolbar. Those can be rearranged as you wish.
+==== Multiple Views
+You can create multiple views by clicking on the "New View" button on the very
+left of the toolbar. These can be rearranged as you wish.
+
+==== Session Saving/Restoring
+When closing PulseView, it automatically saves the sessions you currently have
+open, including the signal configuration and any protocol decoders you might
+have added. The next time you start it again, it'll be restored to its
+previous state.
+
+This metadata is also saved with every .sr file you save so that the next time
+you open the .sr file, your signal configurations, views and decoders are
+restored. These metadata files have the ending .pvs (PulseView Setup) and can
+be edited in any text editor if you wish to change something manually.
+
+Additionally, you can save or load this metadata at any time using the
+save/load buttons.
index 001b6e9f2eb1dc576305e7c0e813a25b0d64b116..6187dbb986be218e164fccd0d7e436efc06e23c8 100644 (file)
@@ -23,6 +23,14 @@ Example:
 
        pulseview -i data.csv -I csv:samplerate=3000000
 
+If you previously saved a PulseView session setup alongside your input file, PulseView will
+automatically load those settings so long as the setup file (.pvs) has the same base name
+as your input file.
+You can also manually specify a PulseView session setup file to load with -s / --settings.
+Example:
+
+       pulseview -s settings.pvs data.sr
+
 The remaining parameters are mostly for debug purposes:
 
        -V / --version          Shows the release version
index d2ca15abc0d30a273bd3da3881fcd1990eb9017f..6915f82c0292e1f0f7961eae93db14d532f9f210 100644 (file)
@@ -50,14 +50,15 @@ image::pv_decoders_3.png[]
 With the stacked decoder added, we can now see that PulseView has decoded the meaning
 of the I²C commands, so that we don't need to bother searching the reference manual.
 In this view, we can see that the I²C packet was a command to read the date and time,
-which was then reported to be 10.03.2013 23:35:30. 
+which was then reported to be "10.03.2013 23:35:30".
 
-There are all kinds of stacked decoders available, but keep in mind that they're not
-shown in the decoder menu. Stacked decoders require a lower-level decoder first before
-they become stackable. Most of the time, they require either the UART, I²C or SPI decoder.
+In this example, we added the I²C and DS1307 decoders separately. However, when opening
+the decoder selector window, you can also double-click on the DS1307 decoder and PulseView
+will try to auto-resolve the dependencies needed to use this decoder. In case there are
+ambiguities (e.g. when several different protocol decoders offer 'uart' output), it will
+ask you to choose which one to use.
 
-You can check the https://sigrok.org/wiki/Protocol_decoders[List of Protocol Decoders]
-to see which protocol decoders have been created already.
+For a list of available and planned protocol decoders, you can https://sigrok.org/wiki/Protocol_decoders[check the wiki].
 
 === Using Decoders on Analog Signals
 
@@ -78,6 +79,48 @@ as you can now visually understand where the ranges for high and low are placed.
 Aside from the default conversion threshold(s), you can choose from a few common presets
 or enter custom values as well. They take the form "0.0V" and "0.0V/0.0V", respectively.
 
+=== Per-row Settings and Actions
+
+Sometimes, you don't want to see all protocol decoder rows or all of the annotation classes
+available in a row. To do so, simply click on the arrow or label of the row you want to
+customize.
+
+image::pv_class_selectors.png[]
+
+From that menu, you can either show/hide the entire row or choose the annotation classes
+you want to see. Everything is visible by default but if you want to focus on specific
+protocol messages or status annotations like warnings or errors, this should help.
+
+Also, if you are examining really long traces, disabling annotations for the most-often
+occuring class (e.g. bit annotations for SPI) then drawing performance will increase, too.
+
+=== Binary Decoder Output
+
+While all protocol decoders create visible annotations, some of them also create binary
+output data which isn't immediately visible at the moment. However, you can examine it
+by opening the Binary Decoder Output View as shown below.
+
+image::pv_binary_decoder_output_view.png[]
+
+Once opened, you need to select a decoder with binary output for it to show anything -
+among which are I2C, I2S, EEPROM24xx, SPI and UART. Having acquired some I2S data and
+using the I2S protocol decoder lets you have the sound data as raw .wav file data, for
+example:
+
+image::pv_binary_decoder_output_view_i2s.png[]
+
+Using the save icon at the top then lets you save this data either as a binary file
+(in this case creating a valid .wav file) or various types of hex dumps. If you want to
+only save a certain part of the binary data, simply select that part before saving.
+
+You may have noticed that the bytes are grouped by color somehow. The meaning behind
+this is that every chunk of bytes emitted by the protocol decoder receives one color,
+the next chunk another color and so on. As there are currently three colors, the cycle
+repeats. This makes it easier to visually organize the data that you see - in the case
+of the I2S decoder, the header has one color because it's sent out in one go and
+following that, every sample for left/right consists of 4 bytes with the same color
+since they're sent out one by one.
+
 === Troubleshooting
 
 In case a protocol decoder doesn't provide the expected result, there are several things
@@ -87,29 +130,29 @@ The first check you should perform is whether the time unit in the ruler
 is given as "sa". This is short for "samples" and means that the device didn't provide
 a sample rate and so PulseView has no way of showing a time scale in seconds or
 fractions thereof. While some decoders can run without timing information, or only
-optionally make use of the time scale, others may not be able to interpret the
-input data since timing information is an essential part of the very protocol.
+optionally make use of it, others may not be able to interpret the input data since
+timing information may be an essential part of that protocol.
 
 Another issue to remain aware of is that decoders need enough samples per protocol step
 to reliably interpret the information. In typical cases the minimum sample rate should
-be four to five times the rate of the fastest activity in the protocol.
+be 4-5 times the rate of the fastest activity in the protocol (e.g. its clock signal).
 
 If a protocol decoder runs but shows you annotations that don't seem to make any sense,
 it's worth double-checking the decoder settings. One common source of error is the
 baud rate. For example, the CAN protocol decoder doesn't know what baud rate
 is used on the bus that you captured, so it could be that a different baud rate is used
-than the one you set. Also, if this is still not the reason for the malfunction, it's
-worth checking whether any of the signals have been captured inverted. Again using the
-CAN bus as an example, the decoder will decode the signal just fine if it's inverted but
+than the one you set. If this is still not the reason for the malfunction, it's worth
+checking whether any of the signals have been captured inverted. Again using the CAN
+bus as an example, the decoder will decode the signal just fine if it's inverted but
 it'll show data even when the signal looks "idle".
 
 When a protocol decoder stops execution because of an unmet constraint (required input
 not connected, essential parameter not specified) or a bug in the decoder itself, you
 will be presented a static red message in the protocol decoder's display area.
-In that case, you check the log output in the settings menu. There you'll find the Python
-error description which you can use to either adjust the configuration,
-or debug the decoder (and let us know of the fix) or you can copy that information and
-file a bug report so that we can fix it.
+In that case, you can check the log output in the settings menu. There you'll find the
+Python error description which you can use to either adjust the configuration,
+debug the decoder (and let us know of the fix) or create a bug report so that we can
+fix it.
 
 Further helpful knowledge and explanations on logic analyzers can be found in our
 https://sigrok.org/wiki/FAQ#Where_can_I_learn_more_about_logic_analyzers.3F["Learn about logic analyzers" FAQ item].
@@ -121,12 +164,19 @@ can do so by right-clicking into the area of the decode signal (not on the signa
 on the left). You are shown several export methods to choose from, with the last one
 being only available if the cursor is enabled.
 
+image::pv_ann_export_menu.png[]
+
 After you chose a method that suits your needs, you are prompted for a file to export
 the annotations to. The contents of the file very much depend on the option you chose
 but also on the annotation export format string that you can define in the _Decoders_
 menu of the settings dialog. If the default output isn't useful to you, you can
 customize it there.
 
+image::pv_ann_export_format.png[]
+
+For example, the string "%s %d: %1" will generate this type of output for the DS1307
+RTC clock protocol decoder: "253-471 DS1307: Read date/time: Sunday, 10.03.2013 23:35:30"
+
 === Creating a Protocol Decoder
 
 Protocol decoders are written in Python and can be created using nothing more than a
index 047333ed13b8aba18b306505d66e97ee6c275b91..f890a565c34aa1fa6da6080c277d4b96fd5c5212 100644 (file)
@@ -36,7 +36,7 @@ delete the AppImage. If you also want the stored settings gone, delete ~/.config
 _[install dependencies https://sigrok.org/wiki/Linux#Building[as listed on the wiki]]_
 mkdir ~/sr
 cd ~/sr
-wget 'https://sigrok.org/gitweb/?p=sigrok-util.git;a=blob_plain;f=cross-compile/linux/sigrok-cross-linux' -O sigrok-cross-linux
+wget -O sigrok-cross-linux "'https://sigrok.org/gitweb/?p=sigrok-util.git;a=blob_plain;f=cross-compile/linux/sigrok-cross-linux'"
 chmod u+x sigrok-cross-linux
 ./sigrok-cross-linux
 export LD_LIBRARY_PATH=~/sr/lib
@@ -58,9 +58,9 @@ Here's how you install them:
 [listing, subs="normal"]
 sudo bash
 cd /etc/udev/rules.d/
-wget 'https://sigrok.org/gitweb/?p=libsigrok.git;a=blob_plain;f=contrib/60-libsigrok.rules' -O 60-libsigrok.rules
-wget 'https://sigrok.org/gitweb/?p=libsigrok.git;a=blob_plain;f=contrib/61-libsigrok-plugdev.rules' -O 61-libsigrok-plugdev.rules
-wget 'https://sigrok.org/gitweb/?p=libsigrok.git;a=blob_plain;f=contrib/61-libsigrok-uaccess.rules' -O 61-libsigrok-uaccess.rules
+wget -O 60-libsigrok.rules "'https://sigrok.org/gitweb/?p=libsigrok.git;a=blob_plain;f=contrib/60-libsigrok.rules'"
+wget -O 61-libsigrok-plugdev.rules "'https://sigrok.org/gitweb/?p=libsigrok.git;a=blob_plain;f=contrib/61-libsigrok-plugdev.rules'"
+wget -O 61-libsigrok-uaccess.rules "'https://sigrok.org/gitweb/?p=libsigrok.git;a=blob_plain;f=contrib/61-libsigrok-uaccess.rules'"
 sudo udevadm control --reload-rules
 --
 
index bfd87f854d19f9b0e0c2ce9725e11d6d66ef21a5..bde7575022d51fc6269d99898bae6446466611a1 100644 (file)
@@ -15,7 +15,7 @@ as $5. These can easily be found by searching for _24MHz Logic Analyzer_. There
 Cypress FX2 boards such as the Lcsoft Mini Board, which can usually be found by searching for
 _Cypress FX2 Board_ or similar.
 
-In addition, a good set of https://sigrok.org/wiki/Probe_comparison[quality probe hooks] is recommended.
+Additionally, a good set of https://sigrok.org/wiki/Probe_comparison[quality probe hooks] is recommended.
 
 Aside from FX2-based logic analyzers, sigrok also supports FX2-based oscilloscopes such as the
 https://sigrok.org/wiki/Hantek_6022BE[Hantek 6022BE], non-FX2 devices like the
index 33e36100b075b92783de804a2dd841ce9b1e8f8b..4427a759cf3266060af90815b94ee6dfadebd908 100644 (file)
@@ -9,6 +9,7 @@
        <file>icons/document-new.png</file>
        <file>icons/document-open.png</file>
        <file>icons/document-save-as.png</file>
+       <file>icons/edit-paste.svg</file>
        <file>icons/help-browser.png</file>
        <file>icons/information.svg</file>
        <file>icons/media-playback-pause.png</file>
@@ -19,6 +20,7 @@
        <file>icons/settings-views.svg</file>
        <file>icons/pulseview.png</file>
        <file>icons/pulseview.svg</file>
+       <file>icons/search.svg</file>
        <file>icons/status-green.svg</file>
        <file>icons/status-grey.svg</file>
        <file>icons/status-red.svg</file>
index 5a6e28a1da996034aa24218df7ef2d66eb425543..6f666c5c308762c55ea0990297f1ea9fea1536eb 100644 (file)
  * along with this program; if not, see <http://www.gnu.org/licenses/>.
  */
 
-#include "application.hpp"
-#include "config.h"
-
 #include <iostream>
 #include <typeinfo>
 
 #include <QDebug>
+#include <QDir>
+#include <QLibraryInfo>
+#include <QMessageBox>
+#include <QWidget>
 
 #include <boost/version.hpp>
 
 #include <libsigrokdecode/libsigrokdecode.h>
 #endif
 
+#include "application.hpp"
+#include "config.h"
+#include "globalsettings.hpp"
+
 using std::cout;
 using std::endl;
 using std::exception;
@@ -60,6 +65,74 @@ Application::Application(int &argc, char* argv[]) :
        setOrganizationDomain("sigrok.org");
 }
 
+QStringList Application::get_languages()
+{
+       QStringList files = QDir(":/l10n/").entryList(QStringList("*.qm"), QDir::Files);
+
+       QStringList result;
+       result << "en";  // Add default language to the set
+
+       // Remove file extensions
+       for (const QString& file : files)
+               result << file.split(".").front();
+
+       result.sort(Qt::CaseInsensitive);
+
+       return result;
+}
+
+void Application::switch_language(const QString& language)
+{
+       removeTranslator(&app_translator_);
+       removeTranslator(&qt_translator_);
+       removeTranslator(&qtbase_translator_);
+
+       if ((language != "C") && (language != "en")) {
+               // Application translations
+               QString resource = ":/l10n/" + language +".qm";
+               if (app_translator_.load(resource))
+                       installTranslator(&app_translator_);
+               else
+                       qWarning() << "Translation resource" << resource << "not found";
+
+               // Qt translations
+               QString tr_path(QLibraryInfo::location(QLibraryInfo::TranslationsPath));
+
+               if (qt_translator_.load("qt_" + language, tr_path))
+                       installTranslator(&qt_translator_);
+               else
+                       qWarning() << "QT translations for" << language << "not found at" <<
+                               tr_path << ", Qt translations package is probably missing";
+
+               // Qt base translations
+               if (qtbase_translator_.load("qtbase_" + language, tr_path))
+                       installTranslator(&qtbase_translator_);
+               else
+                       qWarning() << "QT base translations for" << language << "not found at" <<
+                               tr_path << ", Qt translations package is probably missing";
+       }
+
+       if (!topLevelWidgets().empty()) {
+               // Force all windows to update
+               for (QWidget *widget : topLevelWidgets())
+                       widget->update();
+
+               QMessageBox msg(topLevelWidgets().front());
+               msg.setText(tr("Some parts of the application may still " \
+                               "use the previous language. Re-opening the affected windows or " \
+                               "restarting the application will remedy this."));
+               msg.setStandardButtons(QMessageBox::Ok);
+               msg.setIcon(QMessageBox::Information);
+               msg.exec();
+       }
+}
+
+void Application::on_setting_changed(const QString &key, const QVariant &value)
+{
+       if (key == pv::GlobalSettings::Key_General_Language)
+               switch_language(value.toString());
+}
+
 void Application::collect_version_info(shared_ptr<sigrok::Context> context)
 {
        // Library versions and features
index c618f80d9c1abeee91a1ee61e54d46d8d3c7b9b3..61fe46e2480dc855210e586230c2e6960e070101 100644 (file)
 #include <vector>
 
 #include <QApplication>
+#include <QStringList>
+#include <QTranslator>
 
 #include <libsigrokcxx/libsigrokcxx.hpp>
 
+#include "globalsettings.hpp"
+
 using std::shared_ptr;
 using std::pair;
 using std::vector;
 
-class Application : public QApplication
+class Application : public QApplication, public pv::GlobalSettingsInterface
 {
        Q_OBJECT
 
 public:
        Application(int &argc, char* argv[]);
 
+       QStringList get_languages();
+       void switch_language(const QString& language);
+       void on_setting_changed(const QString &key, const QVariant &value);
+
        void collect_version_info(shared_ptr<sigrok::Context> context);
        void print_version_info();
 
@@ -58,6 +66,8 @@ private:
        vector< pair<QString, QString> > input_format_list_;
        vector< pair<QString, QString> > output_format_list_;
        vector< pair<QString, QString> > pd_list_;
+
+       QTranslator app_translator_, qt_translator_, qtbase_translator_;
 };
 
 #endif // PULSEVIEW_PV_APPLICATION_HPP
index 9735e14681edaa6d9f1b6bdd642f76ba8db694d8..1f17aefdd530be1029b81276fb480d811f36e8af 100644 (file)
@@ -82,6 +82,7 @@ void Binding::add_properties_to_form(QFormLayout *layout, bool auto_commit)
                        help_lbl = new QLabel(p->desc());
                        help_lbl->setVisible(false);
                        help_lbl->setWordWrap(true);
+                       help_lbl->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
                        help_labels_[help_btn] = help_lbl;
                }
 
index f51f90774afd0561d130704819528f9fd887a0f1..80725d2a8b35248461ca1c14e57c3d39f5f529d8 100644 (file)
@@ -54,12 +54,11 @@ Decoder::Decoder(
 {
        assert(decoder_);
 
-       const srd_decoder *const dec = decoder_->decoder();
+       const srd_decoder *const dec = decoder_->get_srd_decoder();
        assert(dec);
 
        for (GSList *l = dec->options; l; l = l->next) {
-               const srd_decoder_option *const opt =
-                       (srd_decoder_option*)l->data;
+               const srd_decoder_option *const opt = (srd_decoder_option*)l->data;
 
                const QString name = QString::fromUtf8(opt->desc);
 
@@ -115,12 +114,11 @@ Glib::VariantBase Decoder::getter(const char *id)
        if (iter != options.end())
                val = (*iter).second;
        else {
-               assert(decoder_->decoder());
+               assert(decoder_->get_srd_decoder());
 
                // Get the default value if not
-               for (GSList *l = decoder_->decoder()->options; l; l = l->next) {
-                       const srd_decoder_option *const opt =
-                               (srd_decoder_option*)l->data;
+               for (GSList *l = decoder_->get_srd_decoder()->options; l; l = l->next) {
+                       const srd_decoder_option *const opt = (srd_decoder_option*)l->data;
                        if (strcmp(opt->id, id) == 0) {
                                val = opt->def;
                                break;
index e983b0df1c3e971ba7942c9ccadb86ed10e09bbb..8ac8d5f9e4970e193771cffe107c414da73f077a 100644 (file)
@@ -24,7 +24,8 @@ extern "C" {
 #include <cassert>
 #include <vector>
 
-#include "annotation.hpp"
+#include <pv/data/decode/annotation.hpp>
+#include <pv/data/decode/decoder.hpp>
 
 using std::vector;
 
@@ -42,13 +43,51 @@ Annotation::Annotation(const srd_proto_data *const pdata, const Row *row) :
                (const srd_proto_data_annotation*)pdata->data;
        assert(pda);
 
-       ann_class_ = (Class)(pda->ann_class);
+       ann_class_id_ = (Class)(pda->ann_class);
+
+       annotations_ = new vector<QString>();
 
        const char *const *annotations = (char**)pda->ann_text;
        while (*annotations) {
-               annotations_.push_back(QString::fromUtf8(*annotations));
+               annotations_->push_back(QString::fromUtf8(*annotations));
                annotations++;
        }
+
+       annotations_->shrink_to_fit();
+}
+
+Annotation::Annotation(Annotation&& a) :
+       start_sample_(a.start_sample_),
+       end_sample_(a.end_sample_),
+       annotations_(a.annotations_),
+       row_(a.row_),
+       ann_class_id_(a.ann_class_id_)
+{
+       a.annotations_ = nullptr;
+}
+
+Annotation& Annotation::operator=(Annotation&& a)
+{
+       if (&a != this) {
+               if (annotations_)
+                       delete annotations_;
+
+               start_sample_ = a.start_sample_;
+               end_sample_ = a.end_sample_;
+               annotations_ = a.annotations_;
+               row_ = a.row_;
+               ann_class_id_ = a.ann_class_id_;
+
+               a.annotations_ = nullptr;
+       }
+
+       return *this;
+}
+
+Annotation::~Annotation()
+{
+       if (annotations_)
+               delete annotations_;
 }
 
 uint64_t Annotation::start_sample() const
@@ -61,12 +100,20 @@ uint64_t Annotation::end_sample() const
        return end_sample_;
 }
 
-Annotation::Class Annotation::ann_class() const
+Annotation::Class Annotation::ann_class_id() const
 {
-       return ann_class_;
+       return ann_class_id_;
+}
+
+const QString Annotation::ann_class_name() const
+{
+       const AnnotationClass* ann_class =
+               row_->decoder()->get_ann_class_by_id(ann_class_id_);
+
+       return QString(ann_class->name);
 }
 
-const vector<QString>& Annotation::annotations() const
+const vector<QString>* Annotation::annotations() const
 {
        return annotations_;
 }
index 8b91c4f47e066605e307ed0301fbd1871d59436d..cfa5e5e982e54746f79cac80c15062af7cd3f66b 100644 (file)
@@ -42,11 +42,17 @@ public:
 
 public:
        Annotation(const srd_proto_data *const pdata, const Row *row);
+       Annotation(Annotation&& a);
+       Annotation& operator=(Annotation&& a);
+       ~Annotation();
 
        uint64_t start_sample() const;
        uint64_t end_sample() const;
-       Class ann_class() const;
-       const vector<QString>& annotations() const;
+
+       Class ann_class_id() const;
+       const QString ann_class_name() const;
+
+       const vector<QString>* annotations() const;
        const Row* row() const;
 
        bool operator<(const Annotation &other) const;
@@ -54,9 +60,9 @@ public:
 private:
        uint64_t start_sample_;
        uint64_t end_sample_;
-       Class ann_class_;
-       vector<QString> annotations_;
+       vector<QString>* annotations_;
        const Row *row_;
+       Class ann_class_id_;
 };
 
 } // namespace decode
index f86c5d08dc86d9261718ab92e2005b1b30dbd914..e6fe82d22a092d7e663a06e813ab52a4aac908da 100644 (file)
@@ -29,7 +29,6 @@
 #include <pv/data/signalbase.hpp>
 #include <pv/data/decodesignal.hpp>
 
-using pv::data::DecodeChannel;
 using std::map;
 using std::string;
 
@@ -38,10 +37,52 @@ namespace data {
 namespace decode {
 
 Decoder::Decoder(const srd_decoder *const dec) :
-       decoder_(dec),
-       shown_(true),
+       srd_decoder_(dec),
+       visible_(true),
        decoder_inst_(nullptr)
 {
+       // Query the annotation output classes
+       uint32_t i = 0;
+       for (GSList *l = dec->annotations; l; l = l->next) {
+               char **ann_class = (char**)l->data;
+               char *name = ann_class[0];
+               char *desc = ann_class[1];
+               ann_classes_.push_back({i++, name, desc, nullptr, true}); // Visible by default
+       }
+
+       // Query the binary output classes
+       i = 0;
+       for (GSList *l = dec->binary; l; l = l->next) {
+               char **bin_class = (char**)l->data;
+               char *name = bin_class[0];
+               char *desc = bin_class[1];
+               bin_classes_.push_back({i++, name, desc});
+       }
+
+       // Query the annotation rows and reference them by the classes that use them
+       uint32_t row_count = 0;
+       for (const GSList *rl = srd_decoder_->annotation_rows; rl; rl = rl->next)
+               row_count++;
+       rows_.reserve(row_count);
+
+       i = 0;
+       for (const GSList *rl = srd_decoder_->annotation_rows; rl; rl = rl->next) {
+               const srd_decoder_annotation_row *const srd_row = (srd_decoder_annotation_row *)rl->data;
+               assert(srd_row);
+               rows_.emplace_back(i++, this, srd_row);
+
+               // FIXME PV can crash from .at() if a PD's ann classes are defined incorrectly
+               for (const GSList *cl = srd_row->ann_classes; cl; cl = cl->next)
+                       ann_classes_.at((size_t)cl->data).row = &(rows_.back());
+       }
+
+       if (rows_.empty()) {
+               // Make sure there is a row for PDs without row declarations
+               rows_.emplace_back(0, this);
+
+               for (AnnotationClass& c : ann_classes_)
+                       c.row = &(rows_.back());
+       }
 }
 
 Decoder::~Decoder()
@@ -50,19 +91,24 @@ Decoder::~Decoder()
                g_variant_unref(option.second);
 }
 
-const srd_decoder* Decoder::decoder() const
+const srd_decoder* Decoder::get_srd_decoder() const
+{
+       return srd_decoder_;
+}
+
+const char* Decoder::name() const
 {
-       return decoder_;
+       return srd_decoder_->name;
 }
 
-bool Decoder::shown() const
+bool Decoder::visible() const
 {
-       return shown_;
+       return visible_;
 }
 
-void Decoder::show(bool show)
+void Decoder::set_visible(bool visible)
 {
-       shown_ = show;
+       visible_ = visible;
 }
 
 const vector<DecodeChannel*>& Decoder::channels() const
@@ -132,7 +178,7 @@ srd_decoder_inst* Decoder::create_decoder_inst(srd_session *session)
        if (decoder_inst_)
                qDebug() << "WARNING: previous decoder instance" << decoder_inst_ << "exists";
 
-       decoder_inst_ = srd_inst_new(session, decoder_->id, opt_hash);
+       decoder_inst_ = srd_inst_new(session, srd_decoder_->id, opt_hash);
        g_hash_table_destroy(opt_hash);
 
        if (!decoder_inst_)
@@ -172,6 +218,70 @@ void Decoder::invalidate_decoder_inst()
        decoder_inst_ = nullptr;
 }
 
+vector<Row*> Decoder::get_rows()
+{
+       vector<Row*> result;
+
+       for (Row& row : rows_)
+               result.push_back(&row);
+
+       return result;
+}
+
+Row* Decoder::get_row_by_id(size_t id)
+{
+       if (id > rows_.size())
+               return nullptr;
+
+       return &(rows_[id]);
+}
+
+vector<const AnnotationClass*> Decoder::ann_classes() const
+{
+       vector<const AnnotationClass*> result;
+
+       for (const AnnotationClass& c : ann_classes_)
+               result.push_back(&c);
+
+       return result;
+}
+
+vector<AnnotationClass*> Decoder::ann_classes()
+{
+       vector<AnnotationClass*> result;
+
+       for (AnnotationClass& c : ann_classes_)
+               result.push_back(&c);
+
+       return result;
+}
+
+AnnotationClass* Decoder::get_ann_class_by_id(size_t id)
+{
+       if (id >= ann_classes_.size())
+               return nullptr;
+
+       return &(ann_classes_[id]);
+}
+
+const AnnotationClass* Decoder::get_ann_class_by_id(size_t id) const
+{
+       if (id >= ann_classes_.size())
+               return nullptr;
+
+       return &(ann_classes_[id]);
+}
+
+uint32_t Decoder::get_binary_class_count() const
+{
+       return bin_classes_.size();
+}
+
+const DecodeBinaryClassInfo* Decoder::get_binary_class(uint32_t id) const
+{
+       return &(bin_classes_.at(id));
+}
+
 }  // namespace decode
 }  // namespace data
 }  // namespace pv
index 55742c6070e5a020c00042b2c8894e744b13986e..eb9a44bf6c1a8841cba632bc1bf3b5aa8d517c91 100644 (file)
@@ -27,6 +27,9 @@
 
 #include <glib.h>
 
+#include <pv/data/signalbase.hpp>
+#include <pv/data/decode/row.hpp>
+
 using std::map;
 using std::string;
 using std::vector;
@@ -40,12 +43,42 @@ namespace pv {
 
 namespace data {
 
-struct DecodeChannel;
 class Logic;
 class SignalBase;
 
 namespace decode {
 
+class Decoder;
+
+struct AnnotationClass
+{
+       size_t id;
+       char* name;
+       char* description;
+       Row* row;
+       bool visible;
+};
+
+struct DecodeChannel
+{
+       uint16_t id;     ///< Global numerical ID for the decode channels in the stack
+       uint16_t bit_id; ///< Tells which bit within a sample represents this channel
+       const bool is_optional;
+       const pv::data::SignalBase *assigned_signal;
+       const QString name, desc;
+       int initial_pin_state;
+       const shared_ptr<Decoder> decoder_;
+       const srd_channel *pdch_;
+};
+
+struct DecodeBinaryClassInfo
+{
+       uint32_t bin_class_id;
+       char* name;
+       char* description;
+};
+
+
 class Decoder
 {
 public:
@@ -53,13 +86,15 @@ public:
 
        virtual ~Decoder();
 
-       const srd_decoder* decoder() const;
+       const srd_decoder* get_srd_decoder() const;
 
-       bool shown() const;
-       void show(bool show = true);
+       const char* name() const;
 
-       const vector<data::DecodeChannel*>& channels() const;
-       void set_channels(vector<data::DecodeChannel*> channels);
+       bool visible() const;
+       void set_visible(bool visible);
+
+       const vector<DecodeChannel*>& channels() const;
+       void set_channels(vector<DecodeChannel*> channels);
 
        const map<string, GVariant*>& options() const;
 
@@ -72,12 +107,26 @@ public:
        srd_decoder_inst* create_decoder_inst(srd_session *session);
        void invalidate_decoder_inst();
 
+       vector<Row*> get_rows();
+       Row* get_row_by_id(size_t id);
+
+       vector<const AnnotationClass*> ann_classes() const;
+       vector<AnnotationClass*> ann_classes();
+       AnnotationClass* get_ann_class_by_id(size_t id);
+       const AnnotationClass* get_ann_class_by_id(size_t id) const;
+
+       uint32_t get_binary_class_count() const;
+       const DecodeBinaryClassInfo* get_binary_class(uint32_t id) const;
+
 private:
-       const srd_decoder *const decoder_;
+       const srd_decoder* const srd_decoder_;
 
-       bool shown_;
+       bool visible_;
 
-       vector<data::DecodeChannel*> channels_;
+       vector<DecodeChannel*> channels_;
+       vector<Row> rows_;
+       vector<AnnotationClass> ann_classes_;
+       vector<DecodeBinaryClassInfo> bin_classes_;
        map<string, GVariant*> options_;
        srd_decoder_inst *decoder_inst_;
 };
index 8195f3e2d00f023c0cdfb91f4b0464c8065d5eab..f1895ae4ef7c71bb100c46d645fc356cc4d78b57 100644 (file)
@@ -17,6 +17,9 @@
  * along with this program; if not, see <http://www.gnu.org/licenses/>.
  */
 
+#include <cassert>
+
+#include "decoder.hpp"
 #include "row.hpp"
 
 #include <libsigrokdecode/libsigrokdecode.h>
@@ -26,57 +29,107 @@ namespace data {
 namespace decode {
 
 Row::Row() :
+       index_(0),
        decoder_(nullptr),
-       row_(nullptr)
+       srd_row_(nullptr),
+       visible_(true)
 {
 }
 
-Row::Row(int index, const srd_decoder *decoder, const srd_decoder_annotation_row *row) :
+Row::Row(uint32_t index, Decoder* decoder, const srd_decoder_annotation_row* srd_row) :
        index_(index),
        decoder_(decoder),
-       row_(row)
+       srd_row_(srd_row),
+       visible_(true)
 {
 }
 
-const srd_decoder* Row::decoder() const
+const Decoder* Row::decoder() const
 {
        return decoder_;
 }
 
-const srd_decoder_annotation_row* Row::row() const
+const srd_decoder_annotation_row* Row::get_srd_row() const
 {
-       return row_;
+       return srd_row_;
 }
 
 const QString Row::title() const
 {
-       if (decoder_ && decoder_->name && row_ && row_->desc)
+       if (decoder_ && decoder_->name() && srd_row_ && srd_row_->desc)
                return QString("%1: %2")
-                       .arg(QString::fromUtf8(decoder_->name),
-                            QString::fromUtf8(row_->desc));
-       if (decoder_ && decoder_->name)
-               return QString::fromUtf8(decoder_->name);
-       if (row_ && row_->desc)
-               return QString::fromUtf8(row_->desc);
+                       .arg(QString::fromUtf8(decoder_->name()),
+                            QString::fromUtf8(srd_row_->desc));
+       if (decoder_ && decoder_->name())
+               return QString::fromUtf8(decoder_->name());
+       if (srd_row_ && srd_row_->desc)
+               return QString::fromUtf8(srd_row_->desc);
+
        return QString();
 }
 
-const QString Row::class_name() const
+const QString Row::description() const
 {
-       if (row_ && row_->desc)
-               return QString::fromUtf8(row_->desc);
+       if (srd_row_ && srd_row_->desc)
+               return QString::fromUtf8(srd_row_->desc);
        return QString();
 }
 
-int Row::index() const
+vector<AnnotationClass*> Row::ann_classes() const
+{
+       assert(decoder_);
+
+       vector<AnnotationClass*> result;
+
+       if (!srd_row_) {
+               if (index_ == 0) {
+                       // When operating as the fallback row, all annotation classes belong to it
+                       return decoder_->ann_classes();
+               }
+               return result;
+       }
+
+       for (GSList *l = srd_row_->ann_classes; l; l = l->next) {
+               size_t class_id = (size_t)l->data;
+               result.push_back(decoder_->get_ann_class_by_id(class_id));
+       }
+
+       return result;
+}
+
+uint32_t Row::index() const
 {
        return index_;
 }
 
-bool Row::operator<(const Row &other) const
+bool Row::visible() const
+{
+       return visible_;
+}
+
+void Row::set_visible(bool visible)
+{
+       visible_ = visible;
+}
+
+bool Row::has_hidden_classes() const
+{
+       for (const AnnotationClass* c : ann_classes())
+               if (!c->visible)
+                       return true;
+
+       return false;
+}
+
+bool Row::operator<(const Row& other) const
 {
        return (decoder_ < other.decoder_) ||
-               (decoder_ == other.decoder_ && row_ < other.row_);
+               (decoder_ == other.decoder_ && srd_row_ < other.srd_row_);
+}
+
+bool Row::operator==(const Row& other) const
+{
+       return ((decoder_ == other.decoder()) && (srd_row_ == other.srd_row_));
 }
 
 }  // namespace decode
index 34bb2373e844babd768335d4d8a335a227696b0c..b877b58b85b4ca25300dad8d45e12042ad30a7a8 100644 (file)
@@ -22,7 +22,8 @@
 
 #include <vector>
 
-#include "annotation.hpp"
+#include <pv/data/decode/annotation.hpp>
+#include <pv/data/decode/decoder.hpp>
 
 struct srd_decoder;
 struct srd_decoder_annotation_row;
@@ -31,27 +32,38 @@ namespace pv {
 namespace data {
 namespace decode {
 
+struct AnnotationClass;
+class Decoder;
+
 class Row
 {
 public:
        Row();
 
-       Row(int index, const srd_decoder *decoder,
-               const srd_decoder_annotation_row *row = nullptr);
+       Row(uint32_t index, Decoder* decoder,
+               const srd_decoder_annotation_row* srd_row = nullptr);
 
-       const srd_decoder* decoder() const;
-       const srd_decoder_annotation_row* row() const;
+       const Decoder* decoder() const;
+       const srd_decoder_annotation_row* get_srd_row() const;
 
        const QString title() const;
-       const QString class_name() const;
-       int index() const;
+       const QString description() const;
+       vector<AnnotationClass*> ann_classes() const;
+       uint32_t index() const;
+
+       bool visible() const;
+       void set_visible(bool visible);
+
+       bool has_hidden_classes() const;
 
-       bool operator<(const Row &other) const;
+       bool operator<(const Row& other) const;
+       bool operator==(const Row& other) const;
 
 private:
-       int index_;
-       const srd_decoder *decoder_;
-       const srd_decoder_annotation_row *row_;
+       uint32_t index_;
+       Decoder* decoder_;
+       const srd_decoder_annotation_row* srd_row_;
+       bool visible_;
 };
 
 }  // namespace decode
index 2a26169eb6cc0f14adb5a107726c40ee891838b6..7b6ec2d3f8b71bb0728cd1381a2867bb072f55c5 100644 (file)
  * along with this program; if not, see <http://www.gnu.org/licenses/>.
  */
 
-#include "rowdata.hpp"
+#include <cassert>
+
+#include <pv/data/decode/decoder.hpp>
+#include <pv/data/decode/row.hpp>
+#include <pv/data/decode/rowdata.hpp>
 
 using std::vector;
 
@@ -25,6 +29,13 @@ namespace pv {
 namespace data {
 namespace decode {
 
+RowData::RowData(Row* row) :
+       row_(row),
+       prev_ann_start_sample_(0)
+{
+       assert(row);
+}
+
 uint64_t RowData::get_max_sample() const
 {
        if (annotations_.empty())
@@ -32,19 +43,76 @@ uint64_t RowData::get_max_sample() const
        return annotations_.back().end_sample();
 }
 
+uint64_t RowData::get_annotation_count() const
+{
+       return annotations_.size();
+}
+
 void RowData::get_annotation_subset(
-       vector<pv::data::decode::Annotation> &dest,
+       deque<const pv::data::decode::Annotation*> &dest,
        uint64_t start_sample, uint64_t end_sample) const
 {
-       for (const auto& annotation : annotations_)
-               if (annotation.end_sample() > start_sample &&
-                       annotation.start_sample() <= end_sample)
-                       dest.push_back(annotation);
+       // Determine whether we must apply per-class filtering or not
+       bool all_ann_classes_enabled = true;
+       bool all_ann_classes_disabled = true;
+
+       uint32_t max_ann_class_id = 0;
+       for (AnnotationClass* c : row_->ann_classes()) {
+               if (!c->visible)
+                       all_ann_classes_enabled = false;
+               else
+                       all_ann_classes_disabled = false;
+               if (c->id > max_ann_class_id)
+                       max_ann_class_id = c->id;
+       }
+
+       if (all_ann_classes_enabled) {
+               // No filtering, send everyting out as-is
+               for (const auto& annotation : annotations_)
+                       if ((annotation.end_sample() > start_sample) &&
+                               (annotation.start_sample() <= end_sample))
+                               dest.push_back(&annotation);
+       } else {
+               if (!all_ann_classes_disabled) {
+                       // Filter out invisible annotation classes
+                       vector<size_t> class_visible;
+                       class_visible.resize(max_ann_class_id + 1, 0);
+                       for (AnnotationClass* c : row_->ann_classes())
+                               if (c->visible)
+                                       class_visible[c->id] = 1;
+
+                       for (const auto& annotation : annotations_)
+                               if ((class_visible[annotation.ann_class_id()]) &&
+                                       (annotation.end_sample() > start_sample) &&
+                                       (annotation.start_sample() <= end_sample))
+                                       dest.push_back(&annotation);
+               }
+       }
 }
 
-void RowData::emplace_annotation(srd_proto_data *pdata, const Row *row)
+void RowData::emplace_annotation(srd_proto_data *pdata)
 {
-       annotations_.emplace_back(pdata, row);
+       // We insert the annotation in a way so that the annotation list
+       // is sorted by start sample. Otherwise, we'd have to sort when
+       // painting, which is expensive
+
+       if (pdata->start_sample < prev_ann_start_sample_) {
+               // Find location to insert the annotation at
+
+               auto it = annotations_.end();
+               do {
+                       it--;
+               } while ((it->start_sample() > pdata->start_sample) && (it != annotations_.begin()));
+
+               // Allow inserting at the front
+               if (it != annotations_.begin())
+                       it++;
+
+               annotations_.emplace(it, pdata, row_);
+       } else {
+               annotations_.emplace_back(pdata, row_);
+               prev_ann_start_sample_ = pdata->start_sample;
+       }
 }
 
 }  // namespace decode
index 0589ec894eb444fad4f2d0d759c98a1eabc13fc7..01ea94f4586c7390601c28ea61e68c76a5ca960d 100644 (file)
@@ -24,8 +24,9 @@
 
 #include <libsigrokdecode/libsigrokdecode.h>
 
-#include "annotation.hpp"
+#include <pv/data/decode/annotation.hpp>
 
+using std::deque;
 using std::vector;
 
 namespace pv {
@@ -37,24 +38,26 @@ class Row;
 class RowData
 {
 public:
-       RowData() = default;
+       RowData(Row* row);
 
-public:
        uint64_t get_max_sample() const;
 
+       uint64_t get_annotation_count() const;
+
        /**
         * Extracts annotations between the given sample range into a vector.
         * Note: The annotations are unsorted and only annotations that fully
         * fit into the sample range are considered.
         */
-       void get_annotation_subset(
-               vector<pv::data::decode::Annotation> &dest,
+       void get_annotation_subset(deque<const pv::data::decode::Annotation*> &dest,
                uint64_t start_sample, uint64_t end_sample) const;
 
-       void emplace_annotation(srd_proto_data *pdata, const Row *row);
+       void emplace_annotation(srd_proto_data *pdata);
 
 private:
-       vector<Annotation> annotations_;
+       deque<Annotation> annotations_;
+       Row* row_;
+       uint64_t prev_ann_start_sample_;
 };
 
 }  // namespace decode
index 68b17a65d076668b182172bee71f5a30605cb232..f8e719992d59b2df7a1c22664e4772ad55088567 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, see <http://www.gnu.org/licenses/>.
  */
 
+#include <cstring>
 #include <forward_list>
 #include <limits>
 
 #include "decodesignal.hpp"
 #include "signaldata.hpp"
 
-#include <pv/binding/decoder.hpp>
 #include <pv/data/decode/decoder.hpp>
 #include <pv/data/decode/row.hpp>
 #include <pv/globalsettings.hpp>
 #include <pv/session.hpp>
 
-using std::forward_list;
 using std::lock_guard;
-using std::make_pair;
 using std::make_shared;
 using std::min;
 using std::out_of_range;
 using std::shared_ptr;
 using std::unique_lock;
-using pv::data::decode::Annotation;
-using pv::data::decode::Decoder;
-using pv::data::decode::Row;
+using pv::data::decode::AnnotationClass;
+using pv::data::decode::DecodeChannel;
 
 namespace pv {
 namespace data {
@@ -75,24 +72,31 @@ const vector< shared_ptr<Decoder> >& DecodeSignal::decoder_stack() const
        return stack_;
 }
 
-void DecodeSignal::stack_decoder(const srd_decoder *decoder)
+void DecodeSignal::stack_decoder(const srd_decoder *decoder, bool restart_decode)
 {
        assert(decoder);
-       const shared_ptr<Decoder> dec = make_shared<decode::Decoder>(decoder);
 
-       stack_.push_back(dec);
+       // Set name if this decoder is the first in the list or the name is unchanged
+       const srd_decoder* prev_dec = stack_.empty() ? nullptr : stack_.back()->get_srd_decoder();
+       const QString prev_dec_name = prev_dec ? QString::fromUtf8(prev_dec->name) : QString();
 
-       // Set name if this decoder is the first in the list
-       if (stack_.size() == 1)
+       if ((stack_.empty()) || ((stack_.size() > 0) && (name() == prev_dec_name)))
                set_name(QString::fromUtf8(decoder->name));
 
+       const shared_ptr<Decoder> dec = make_shared<Decoder>(decoder);
+       stack_.push_back(dec);
+
        // Include the newly created decode channels in the channel lists
        update_channel_list();
 
        stack_config_changed_ = true;
        auto_assign_signals(dec);
        commit_decoder_channels();
-       begin_decode();
+
+       decoder_stacked((void*)dec.get());
+
+       if (restart_decode)
+               begin_decode();
 }
 
 void DecodeSignal::remove_decoder(int index)
@@ -105,6 +109,8 @@ void DecodeSignal::remove_decoder(int index)
        for (int i = 0; i < index; i++, iter++)
                assert(iter != stack_.end());
 
+       decoder_removed(iter->get());
+
        // Delete the element
        stack_.erase(iter);
 
@@ -125,8 +131,8 @@ bool DecodeSignal::toggle_decoder_visibility(int index)
        // Toggle decoder visibility
        bool state = false;
        if (dec) {
-               state = !dec->shown();
-               dec->show(state);
+               state = !dec->visible();
+               dec->set_visible(state);
        }
 
        return state;
@@ -134,6 +140,8 @@ bool DecodeSignal::toggle_decoder_visibility(int index)
 
 void DecodeSignal::reset_decode(bool shutting_down)
 {
+       resume_decode();  // Make sure the decode thread isn't blocked by pausing
+
        if (stack_config_changed_ || shutting_down)
                stop_srd_session();
        else
@@ -151,9 +159,6 @@ void DecodeSignal::reset_decode(bool shutting_down)
                logic_mux_thread_.join();
        }
 
-       resume_decode();  // Make sure the decode thread isn't blocked by pausing
-
-       class_rows_.clear();
        current_segment_id_ = 0;
        segments_.clear();
 
@@ -200,39 +205,18 @@ void DecodeSignal::begin_decode()
        // Make sure that all assigned channels still provide logic data
        // (can happen when a converted signal was assigned but the
        // conversion removed in the meanwhile)
-       for (data::DecodeChannel& ch : channels_)
+       for (decode::DecodeChannel& ch : channels_)
                if (ch.assigned_signal && !(ch.assigned_signal->logic_data() != nullptr))
                        ch.assigned_signal = nullptr;
 
        // Check that all decoders have the required channels
-       for (const shared_ptr<decode::Decoder>& dec : stack_)
+       for (const shared_ptr<Decoder>& dec : stack_)
                if (!dec->have_required_channels()) {
                        set_error_message(tr("One or more required channels "
                                "have not been specified"));
                        return;
                }
 
-       // Map out all the annotation classes
-       int row_index = 0;
-       for (const shared_ptr<decode::Decoder>& dec : stack_) {
-               assert(dec);
-               const srd_decoder *const decc = dec->decoder();
-               assert(dec->decoder());
-
-               for (const GSList *l = decc->annotation_rows; l; l = l->next) {
-                       const srd_decoder_annotation_row *const ann_row =
-                               (srd_decoder_annotation_row *)l->data;
-                       assert(ann_row);
-
-                       const Row row(row_index++, decc, ann_row);
-
-                       for (const GSList *ll = ann_row->ann_classes;
-                               ll; ll = ll->next)
-                               class_rows_[make_pair(decc,
-                                       GPOINTER_TO_INT(ll->data))] = row;
-               }
-       }
-
        // Free the logic data and its segment(s) if it needs to be updated
        if (logic_mux_data_invalid_)
                logic_mux_data_.reset();
@@ -285,7 +269,7 @@ QString DecodeSignal::error_message() const
        return error_message_;
 }
 
-const vector<data::DecodeChannel> DecodeSignal::get_channels() const
+const vector<decode::DecodeChannel> DecodeSignal::get_channels() const
 {
        return channels_;
 }
@@ -295,7 +279,7 @@ void DecodeSignal::auto_assign_signals(const shared_ptr<Decoder> dec)
        bool new_assignment = false;
 
        // Try to auto-select channels that don't have signals assigned yet
-       for (data::DecodeChannel& ch : channels_) {
+       for (decode::DecodeChannel& ch : channels_) {
                // If a decoder is given, auto-assign only its channels
                if (dec && (ch.decoder_ != dec))
                        continue;
@@ -344,7 +328,7 @@ void DecodeSignal::auto_assign_signals(const shared_ptr<Decoder> dec)
 
 void DecodeSignal::assign_signal(const uint16_t channel_id, const SignalBase *signal)
 {
-       for (data::DecodeChannel& ch : channels_)
+       for (decode::DecodeChannel& ch : channels_)
                if (ch.id == channel_id) {
                        ch.assigned_signal = signal;
                        logic_mux_data_invalid_ = true;
@@ -360,12 +344,12 @@ int DecodeSignal::get_assigned_signal_count() const
 {
        // Count all channels that have a signal assigned to them
        return count_if(channels_.begin(), channels_.end(),
-               [](data::DecodeChannel ch) { return ch.assigned_signal; });
+               [](decode::DecodeChannel ch) { return ch.assigned_signal; });
 }
 
 void DecodeSignal::set_initial_pin_state(const uint16_t channel_id, const int init_state)
 {
-       for (data::DecodeChannel& ch : channels_)
+       for (decode::DecodeChannel& ch : channels_)
                if (ch.id == channel_id)
                        ch.initial_pin_state = init_state;
 
@@ -405,7 +389,7 @@ int64_t DecodeSignal::get_working_sample_count(uint32_t segment_id) const
        int64_t count = std::numeric_limits<int64_t>::max();
        bool no_signals_assigned = true;
 
-       for (const data::DecodeChannel& ch : channels_)
+       for (const decode::DecodeChannel& ch : channels_)
                if (ch.assigned_signal) {
                        no_signals_assigned = false;
 
@@ -431,96 +415,231 @@ int64_t DecodeSignal::get_decoded_sample_count(uint32_t segment_id,
 
        int64_t result = 0;
 
-       try {
-               const DecodeSegment *segment = &(segments_.at(segment_id));
-               if (include_processing)
-                       result = segment->samples_decoded_incl;
-               else
-                       result = segment->samples_decoded_excl;
-       } catch (out_of_range&) {
-               // Do nothing
-       }
+       if (segment_id >= segments_.size())
+               return result;
+
+       if (include_processing)
+               result = segments_[segment_id].samples_decoded_incl;
+       else
+               result = segments_[segment_id].samples_decoded_excl;
 
        return result;
 }
 
-vector<Row> DecodeSignal::visible_rows() const
+vector<Row*> DecodeSignal::get_rows(bool visible_only)
 {
-       lock_guard<mutex> lock(output_mutex_);
-
-       vector<Row> rows;
+       vector<Row*> rows;
 
-       for (const shared_ptr<decode::Decoder>& dec : stack_) {
+       for (const shared_ptr<Decoder>& dec : stack_) {
                assert(dec);
-               if (!dec->shown())
+               if (visible_only && !dec->visible())
                        continue;
 
-               const srd_decoder *const decc = dec->decoder();
-               assert(dec->decoder());
+               for (Row* row : dec->get_rows())
+                       rows.push_back(row);
+       }
 
-               int row_index = 0;
-               // Add a row for the decoder if it doesn't have a row list
-               if (!decc->annotation_rows)
-                       rows.emplace_back(row_index++, decc);
+       return rows;
+}
 
-               // Add the decoder rows
-               for (const GSList *l = decc->annotation_rows; l; l = l->next) {
-                       const srd_decoder_annotation_row *const ann_row =
-                               (srd_decoder_annotation_row *)l->data;
-                       assert(ann_row);
-                       rows.emplace_back(row_index++, decc, ann_row);
-               }
+vector<const Row*> DecodeSignal::get_rows(bool visible_only) const
+{
+       vector<const Row*> rows;
+
+       for (const shared_ptr<Decoder>& dec : stack_) {
+               assert(dec);
+               if (visible_only && !dec->visible())
+                       continue;
+
+               for (const Row* row : dec->get_rows())
+                       rows.push_back(row);
        }
 
        return rows;
 }
 
-void DecodeSignal::get_annotation_subset(
-       vector<pv::data::decode::Annotation> &dest,
-       const decode::Row &row, uint32_t segment_id, uint64_t start_sample,
+
+uint64_t DecodeSignal::get_annotation_count(const Row* row, uint32_t segment_id) const
+{
+       if (segment_id >= segments_.size())
+               return 0;
+
+       const DecodeSegment* segment = &(segments_.at(segment_id));
+
+       auto row_it = segment->annotation_rows.find(row);
+
+       const RowData* rd;
+       if (row_it == segment->annotation_rows.end())
+               return 0;
+       else
+               rd = &(row_it->second);
+
+       return rd->get_annotation_count();
+}
+
+void DecodeSignal::get_annotation_subset(deque<const Annotation*> &dest,
+       const Row* row, uint32_t segment_id, uint64_t start_sample,
        uint64_t end_sample) const
 {
        lock_guard<mutex> lock(output_mutex_);
 
+       if (segment_id >= segments_.size())
+               return;
+
+       const DecodeSegment* segment = &(segments_.at(segment_id));
+
+       auto row_it = segment->annotation_rows.find(row);
+
+       const RowData* rd;
+       if (row_it == segment->annotation_rows.end())
+               return;
+       else
+               rd = &(row_it->second);
+
+       rd->get_annotation_subset(dest, start_sample, end_sample);
+}
+
+void DecodeSignal::get_annotation_subset(deque<const Annotation*> &dest,
+       uint32_t segment_id, uint64_t start_sample, uint64_t end_sample) const
+{
+       for (const Row* row : get_rows())
+               get_annotation_subset(dest, row, segment_id, start_sample, end_sample);
+}
+
+uint32_t DecodeSignal::get_binary_data_chunk_count(uint32_t segment_id,
+       const Decoder* dec, uint32_t bin_class_id) const
+{
+       if (segments_.size() == 0)
+               return 0;
+
        try {
                const DecodeSegment *segment = &(segments_.at(segment_id));
-               const map<const decode::Row, decode::RowData> *rows =
-                       &(segment->annotation_rows);
 
-               const auto iter = rows->find(row);
-               if (iter != rows->end())
-                       (*iter).second.get_annotation_subset(dest,
-                               start_sample, end_sample);
+               for (const DecodeBinaryClass& bc : segment->binary_classes)
+                       if ((bc.decoder == dec) && (bc.info->bin_class_id == bin_class_id))
+                               return bc.chunks.size();
        } catch (out_of_range&) {
                // Do nothing
        }
+
+       return 0;
 }
 
-void DecodeSignal::get_annotation_subset(
-       vector<pv::data::decode::Annotation> &dest,
-       uint32_t segment_id, uint64_t start_sample, uint64_t end_sample) const
+void DecodeSignal::get_binary_data_chunk(uint32_t segment_id,
+       const  Decoder* dec, uint32_t bin_class_id, uint32_t chunk_id,
+       const vector<uint8_t> **dest, uint64_t *size)
 {
-       // Note: We put all vectors and lists on the heap, not the stack
+       try {
+               const DecodeSegment *segment = &(segments_.at(segment_id));
 
-       const vector<Row> rows = visible_rows();
+               for (const DecodeBinaryClass& bc : segment->binary_classes)
+                       if ((bc.decoder == dec) && (bc.info->bin_class_id == bin_class_id)) {
+                               if (dest) *dest = &(bc.chunks.at(chunk_id).data);
+                               if (size) *size = bc.chunks.at(chunk_id).data.size();
+                               return;
+                       }
+       } catch (out_of_range&) {
+               // Do nothing
+       }
+}
 
-       // Use forward_lists for faster merging
-       forward_list<Annotation> *all_ann_list = new forward_list<Annotation>();
+void DecodeSignal::get_merged_binary_data_chunks_by_sample(uint32_t segment_id,
+       const Decoder* dec, uint32_t bin_class_id, uint64_t start_sample,
+       uint64_t end_sample, vector<uint8_t> *dest) const
+{
+       assert(dest != nullptr);
 
-       for (const Row& row : rows) {
-               vector<Annotation> *ann_vector = new vector<Annotation>();
-               get_annotation_subset(*ann_vector, row, segment_id, start_sample, end_sample);
+       try {
+               const DecodeSegment *segment = &(segments_.at(segment_id));
 
-               forward_list<Annotation> *ann_list =
-                       new forward_list<Annotation>(ann_vector->begin(), ann_vector->end());
-               delete ann_vector;
+               const DecodeBinaryClass* bin_class = nullptr;
+               for (const DecodeBinaryClass& bc : segment->binary_classes)
+                       if ((bc.decoder == dec) && (bc.info->bin_class_id == bin_class_id))
+                               bin_class = &bc;
+
+               // Determine overall size before copying to resize dest vector only once
+               uint64_t size = 0;
+               uint64_t matches = 0;
+               for (const DecodeBinaryDataChunk& chunk : bin_class->chunks)
+                       if ((chunk.sample >= start_sample) && (chunk.sample < end_sample)) {
+                               size += chunk.data.size();
+                               matches++;
+                       }
+               dest->resize(size);
+
+               uint64_t offset = 0;
+               uint64_t matches2 = 0;
+               for (const DecodeBinaryDataChunk& chunk : bin_class->chunks)
+                       if ((chunk.sample >= start_sample) && (chunk.sample < end_sample)) {
+                               memcpy(dest->data() + offset, chunk.data.data(), chunk.data.size());
+                               offset += chunk.data.size();
+                               matches2++;
+
+                               // Make sure we don't overwrite memory if the array grew in the meanwhile
+                               if (matches2 == matches)
+                                       break;
+                       }
+       } catch (out_of_range&) {
+               // Do nothing
+       }
+}
+
+void DecodeSignal::get_merged_binary_data_chunks_by_offset(uint32_t segment_id,
+       const Decoder* dec, uint32_t bin_class_id, uint64_t start, uint64_t end,
+       vector<uint8_t> *dest) const
+{
+       assert(dest != nullptr);
 
-               all_ann_list->merge(*ann_list);
-               delete ann_list;
+       try {
+               const DecodeSegment *segment = &(segments_.at(segment_id));
+
+               const DecodeBinaryClass* bin_class = nullptr;
+               for (const DecodeBinaryClass& bc : segment->binary_classes)
+                       if ((bc.decoder == dec) && (bc.info->bin_class_id == bin_class_id))
+                               bin_class = &bc;
+
+               // Determine overall size before copying to resize dest vector only once
+               uint64_t size = 0;
+               uint64_t offset = 0;
+               for (const DecodeBinaryDataChunk& chunk : bin_class->chunks) {
+                       if (offset >= start)
+                               size += chunk.data.size();
+                       offset += chunk.data.size();
+                       if (offset >= end)
+                               break;
+               }
+               dest->resize(size);
+
+               offset = 0;
+               uint64_t dest_offset = 0;
+               for (const DecodeBinaryDataChunk& chunk : bin_class->chunks) {
+                       if (offset >= start) {
+                               memcpy(dest->data() + dest_offset, chunk.data.data(), chunk.data.size());
+                               dest_offset += chunk.data.size();
+                       }
+                       offset += chunk.data.size();
+                       if (offset >= end)
+                               break;
+               }
+       } catch (out_of_range&) {
+               // Do nothing
        }
+}
+
+const DecodeBinaryClass* DecodeSignal::get_binary_data_class(uint32_t segment_id,
+       const Decoder* dec, uint32_t bin_class_id) const
+{
+       try {
+               const DecodeSegment *segment = &(segments_.at(segment_id));
 
-       move(all_ann_list->begin(), all_ann_list->end(), back_inserter(dest));
-       delete all_ann_list;
+               for (const DecodeBinaryClass& bc : segment->binary_classes)
+                       if ((bc.decoder == dec) && (bc.info->bin_class_id == bin_class_id))
+                               return &bc;
+       } catch (out_of_range&) {
+               // Do nothing
+       }
+
+       return nullptr;
 }
 
 void DecodeSignal::save_settings(QSettings &settings) const
@@ -531,17 +650,18 @@ void DecodeSignal::save_settings(QSettings &settings) const
 
        // Save decoder stack
        int decoder_idx = 0;
-       for (const shared_ptr<decode::Decoder>& decoder : stack_) {
+       for (const shared_ptr<Decoder>& decoder : stack_) {
                settings.beginGroup("decoder" + QString::number(decoder_idx++));
 
-               settings.setValue("id", decoder->decoder()->id);
+               settings.setValue("id", decoder->get_srd_decoder()->id);
+               settings.setValue("visible", decoder->visible());
 
                // Save decoder options
                const map<string, GVariant*>& options = decoder->options();
 
                settings.setValue("options", (int)options.size());
 
-               // Note: decode::Decoder::options() returns only the options
+               // Note: Decoder::options() returns only the options
                // that differ from the default. See binding::Decoder::getter()
                int i = 0;
                for (auto& option : options) {
@@ -552,6 +672,24 @@ void DecodeSignal::save_settings(QSettings &settings) const
                        i++;
                }
 
+               // Save row properties
+               i = 0;
+               for (const Row* row : decoder->get_rows()) {
+                       settings.beginGroup("row" + QString::number(i));
+                       settings.setValue("visible", row->visible());
+                       settings.endGroup();
+                       i++;
+               }
+
+               // Save class properties
+               i = 0;
+               for (const AnnotationClass* ann_class : decoder->ann_classes()) {
+                       settings.beginGroup("ann_class" + QString::number(i));
+                       settings.setValue("visible", ann_class->visible);
+                       settings.endGroup();
+                       i++;
+               }
+
                settings.endGroup();
        }
 
@@ -560,7 +698,7 @@ void DecodeSignal::save_settings(QSettings &settings) const
 
        for (unsigned int channel_id = 0; channel_id < channels_.size(); channel_id++) {
                auto channel = find_if(channels_.begin(), channels_.end(),
-                       [&](data::DecodeChannel ch) { return ch.id == channel_id; });
+                       [&](decode::DecodeChannel ch) { return ch.id == channel_id; });
 
                if (channel == channels_.end()) {
                        qDebug() << "ERROR: Gap in channel index:" << channel_id;
@@ -599,10 +737,10 @@ void DecodeSignal::restore_settings(QSettings &settings)
                                continue;
 
                        if (QString::fromUtf8(dec->id) == id) {
-                               shared_ptr<decode::Decoder> decoder =
-                                       make_shared<decode::Decoder>(dec);
+                               shared_ptr<Decoder> decoder = make_shared<Decoder>(dec);
 
                                stack_.push_back(decoder);
+                               decoder->set_visible(settings.value("visible", true).toBool());
 
                                // Restore decoder options that differ from their default
                                int options = settings.value("options").toInt();
@@ -617,6 +755,25 @@ void DecodeSignal::restore_settings(QSettings &settings)
 
                                // Include the newly created decode channels in the channel lists
                                update_channel_list();
+
+                               // Restore row properties
+                               int i = 0;
+                               for (Row* row : decoder->get_rows()) {
+                                       settings.beginGroup("row" + QString::number(i));
+                                       row->set_visible(settings.value("visible", true).toBool());
+                                       settings.endGroup();
+                                       i++;
+                               }
+
+                               // Restore class properties
+                               i = 0;
+                               for (AnnotationClass* ann_class : decoder->ann_classes()) {
+                                       settings.beginGroup("ann_class" + QString::number(i));
+                                       ann_class->visible = settings.value("visible", true).toBool();
+                                       settings.endGroup();
+                                       i++;
+                               }
+
                                break;
                        }
                }
@@ -628,12 +785,12 @@ void DecodeSignal::restore_settings(QSettings &settings)
        // Restore channel mapping
        unsigned int channels = settings.value("channels").toInt();
 
-       const unordered_set< shared_ptr<data::SignalBase> > signalbases =
+       const vector< shared_ptr<data::SignalBase> > signalbases =
                session_.signalbases();
 
        for (unsigned int channel_id = 0; channel_id < channels; channel_id++) {
                auto channel = find_if(channels_.begin(), channels_.end(),
-                       [&](data::DecodeChannel ch) { return ch.id == channel_id; });
+                       [&](decode::DecodeChannel ch) { return ch.id == channel_id; });
 
                if (channel == channels_.end()) {
                        qDebug() << "ERROR: Non-existant channel index:" << channel_id;
@@ -673,7 +830,7 @@ uint32_t DecodeSignal::get_input_segment_count() const
        uint64_t count = std::numeric_limits<uint64_t>::max();
        bool no_signals_assigned = true;
 
-       for (const data::DecodeChannel& ch : channels_)
+       for (const decode::DecodeChannel& ch : channels_)
                if (ch.assigned_signal) {
                        no_signals_assigned = false;
 
@@ -693,7 +850,7 @@ uint32_t DecodeSignal::get_input_samplerate(uint32_t segment_id) const
 {
        double samplerate = 0;
 
-       for (const data::DecodeChannel& ch : channels_)
+       for (const decode::DecodeChannel& ch : channels_)
                if (ch.assigned_signal) {
                        const shared_ptr<Logic> logic_data = ch.assigned_signal->logic_data();
                        if (!logic_data || logic_data->logic_segments().empty())
@@ -711,25 +868,34 @@ uint32_t DecodeSignal::get_input_samplerate(uint32_t segment_id) const
        return samplerate;
 }
 
+Decoder* DecodeSignal::get_decoder_by_instance(const srd_decoder *const srd_dec)
+{
+       for (shared_ptr<Decoder>& d : stack_)
+               if (d->get_srd_decoder() == srd_dec)
+                       return d.get();
+
+       return nullptr;
+}
+
 void DecodeSignal::update_channel_list()
 {
-       vector<data::DecodeChannel> prev_channels = channels_;
+       vector<decode::DecodeChannel> prev_channels = channels_;
        channels_.clear();
 
        uint16_t id = 0;
 
        // Copy existing entries, create new as needed
        for (shared_ptr<Decoder>& decoder : stack_) {
-               const srd_decoder* srd_d = decoder->decoder();
+               const srd_decoder* srd_dec = decoder->get_srd_decoder();
                const GSList *l;
 
                // Mandatory channels
-               for (l = srd_d->channels; l; l = l->next) {
+               for (l = srd_dec->channels; l; l = l->next) {
                        const struct srd_channel *const pdch = (struct srd_channel *)l->data;
                        bool ch_added = false;
 
                        // Copy but update ID if this channel was in the list before
-                       for (data::DecodeChannel& ch : prev_channels)
+                       for (decode::DecodeChannel& ch : prev_channels)
                                if (ch.pdch_ == pdch) {
                                        ch.id = id++;
                                        channels_.push_back(ch);
@@ -739,7 +905,7 @@ void DecodeSignal::update_channel_list()
 
                        if (!ch_added) {
                                // Create new entry without a mapped signal
-                               data::DecodeChannel ch = {id++, 0, false, nullptr,
+                               decode::DecodeChannel ch = {id++, 0, false, nullptr,
                                        QString::fromUtf8(pdch->name), QString::fromUtf8(pdch->desc),
                                        SRD_INITIAL_PIN_SAME_AS_SAMPLE0, decoder, pdch};
                                channels_.push_back(ch);
@@ -747,12 +913,12 @@ void DecodeSignal::update_channel_list()
                }
 
                // Optional channels
-               for (l = srd_d->opt_channels; l; l = l->next) {
+               for (l = srd_dec->opt_channels; l; l = l->next) {
                        const struct srd_channel *const pdch = (struct srd_channel *)l->data;
                        bool ch_added = false;
 
                        // Copy but update ID if this channel was in the list before
-                       for (data::DecodeChannel& ch : prev_channels)
+                       for (decode::DecodeChannel& ch : prev_channels)
                                if (ch.pdch_ == pdch) {
                                        ch.id = id++;
                                        channels_.push_back(ch);
@@ -762,7 +928,7 @@ void DecodeSignal::update_channel_list()
 
                        if (!ch_added) {
                                // Create new entry without a mapped signal
-                               data::DecodeChannel ch = {id++, 0, true, nullptr,
+                               decode::DecodeChannel ch = {id++, 0, true, nullptr,
                                        QString::fromUtf8(pdch->name), QString::fromUtf8(pdch->desc),
                                        SRD_INITIAL_PIN_SAME_AS_SAMPLE0, decoder, pdch};
                                channels_.push_back(ch);
@@ -777,8 +943,8 @@ void DecodeSignal::update_channel_list()
        } else {
                // Same number but assignment may still differ, so compare all channels
                for (size_t i = 0; i < channels_.size(); i++) {
-                       const data::DecodeChannel& p_ch = prev_channels[i];
-                       const data::DecodeChannel& ch = channels_[i];
+                       const decode::DecodeChannel& p_ch = prev_channels[i];
+                       const decode::DecodeChannel& ch = channels_[i];
 
                        if ((p_ch.pdch_ != ch.pdch_) ||
                                (p_ch.assigned_signal != ch.assigned_signal)) {
@@ -795,10 +961,10 @@ void DecodeSignal::update_channel_list()
 void DecodeSignal::commit_decoder_channels()
 {
        // Submit channel list to every decoder, containing only the relevant channels
-       for (shared_ptr<decode::Decoder> dec : stack_) {
-               vector<data::DecodeChannel*> channel_list;
+       for (shared_ptr<Decoder> dec : stack_) {
+               vector<decode::DecodeChannel*> channel_list;
 
-               for (data::DecodeChannel& ch : channels_)
+               for (decode::DecodeChannel& ch : channels_)
                        if (ch.decoder_ == dec)
                                channel_list.push_back(&ch);
 
@@ -807,7 +973,7 @@ void DecodeSignal::commit_decoder_channels()
 
        // Channel bit IDs must be in sync with the channel's apperance in channels_
        int id = 0;
-       for (data::DecodeChannel& ch : channels_)
+       for (decode::DecodeChannel& ch : channels_)
                if (ch.assigned_signal)
                        ch.bit_id = id++;
 }
@@ -824,7 +990,7 @@ void DecodeSignal::mux_logic_samples(uint32_t segment_id, const int64_t start, c
        vector<uint8_t> signal_in_bytepos;
        vector<uint8_t> signal_in_bitpos;
 
-       for (data::DecodeChannel& ch : channels_)
+       for (decode::DecodeChannel& ch : channels_)
                if (ch.assigned_signal) {
                        const shared_ptr<Logic> logic_data = ch.assigned_signal->logic_data();
 
@@ -1110,7 +1276,7 @@ void DecodeSignal::start_srd_session()
                if (samplerate)
                        srd_session_metadata_set(srd_session_, SRD_CONF_SAMPLERATE,
                                g_variant_new_uint64(samplerate));
-               for (const shared_ptr<decode::Decoder>& dec : stack_)
+               for (const shared_ptr<Decoder>& dec : stack_)
                        dec->apply_all_options();
                srd_session_start(srd_session_);
 
@@ -1123,7 +1289,7 @@ void DecodeSignal::start_srd_session()
 
        // Create the decoders
        srd_decoder_inst *prev_di = nullptr;
-       for (const shared_ptr<decode::Decoder>& dec : stack_) {
+       for (const shared_ptr<Decoder>& dec : stack_) {
                srd_decoder_inst *const di = dec->create_decoder_inst(srd_session_);
 
                if (!di) {
@@ -1147,6 +1313,9 @@ void DecodeSignal::start_srd_session()
        srd_pd_output_callback_add(srd_session_, SRD_OUTPUT_ANN,
                DecodeSignal::annotation_callback, this);
 
+       srd_pd_output_callback_add(srd_session_, SRD_OUTPUT_BINARY,
+               DecodeSignal::binary_callback, this);
+
        srd_session_start(srd_session_);
 
        // We just recreated the srd session, so all stack changes are applied now
@@ -1170,7 +1339,7 @@ void DecodeSignal::terminate_srd_session()
                if (samplerate)
                        srd_session_metadata_set(srd_session_, SRD_CONF_SAMPLERATE,
                                g_variant_new_uint64(samplerate));
-               for (const shared_ptr<decode::Decoder>& dec : stack_)
+               for (const shared_ptr<Decoder>& dec : stack_)
                        dec->apply_all_options();
        }
 }
@@ -1183,7 +1352,7 @@ void DecodeSignal::stop_srd_session()
                srd_session_ = nullptr;
 
                // Mark the decoder instances as non-existant since they were deleted
-               for (const shared_ptr<decode::Decoder>& dec : stack_)
+               for (const shared_ptr<Decoder>& dec : stack_)
                        dec->invalidate_decoder_inst();
        }
 }
@@ -1195,7 +1364,7 @@ void DecodeSignal::connect_input_notifiers()
        disconnect(this, SLOT(on_data_received()));
 
        // Connect the currently used signals to our slot
-       for (data::DecodeChannel& ch : channels_) {
+       for (decode::DecodeChannel& ch : channels_) {
                if (!ch.assigned_signal)
                        continue;
 
@@ -1213,29 +1382,17 @@ void DecodeSignal::create_decode_segment()
        segments_.emplace_back(DecodeSegment());
 
        // Add annotation classes
-       for (const shared_ptr<decode::Decoder>& dec : stack_) {
-               assert(dec);
-               const srd_decoder *const decc = dec->decoder();
-               assert(dec->decoder());
-
-               int row_index = 0;
-               // Add a row for the decoder if it doesn't have a row list
-               if (!decc->annotation_rows)
-                       (segments_.back().annotation_rows)[Row(row_index++, decc)] =
-                               decode::RowData();
-
-               // Add the decoder rows
-               for (const GSList *l = decc->annotation_rows; l; l = l->next) {
-                       const srd_decoder_annotation_row *const ann_row =
-                               (srd_decoder_annotation_row *)l->data;
-                       assert(ann_row);
-
-                       const Row row(row_index++, decc, ann_row);
-
-                       // Add a new empty row data object
-                       (segments_.back().annotation_rows)[row] =
-                               decode::RowData();
-               }
+       for (const shared_ptr<Decoder>& dec : stack_)
+               for (Row* row : dec->get_rows())
+                       segments_.back().annotation_rows.emplace(row, RowData(row));
+
+       // Prepare our binary output classes
+       for (const shared_ptr<Decoder>& dec : stack_) {
+               uint32_t n = dec->get_binary_class_count();
+
+               for (uint32_t i = 0; i < n; i++)
+                       segments_.back().binary_classes.push_back(
+                               {dec.get(), dec->get_binary_class(i), deque<DecodeBinaryDataChunk>()});
        }
 }
 
@@ -1252,37 +1409,83 @@ void DecodeSignal::annotation_callback(srd_proto_data *pdata, void *decode_signa
 
        lock_guard<mutex> lock(ds->output_mutex_);
 
-       // Find the row
+       // Get the decoder and the annotation data
        assert(pdata->pdo);
        assert(pdata->pdo->di);
-       const srd_decoder *const decc = pdata->pdo->di->decoder;
-       assert(decc);
+       const srd_decoder *const srd_dec = pdata->pdo->di->decoder;
+       assert(srd_dec);
 
-       const srd_proto_data_annotation *const pda =
-               (const srd_proto_data_annotation*)pdata->data;
+       const srd_proto_data_annotation *const pda = (const srd_proto_data_annotation*)pdata->data;
        assert(pda);
 
-       auto row_iter = ds->segments_.at(ds->current_segment_id_).annotation_rows.end();
-
-       // Try looking up the sub-row of this class
-       const auto format = pda->ann_class;
-       const auto r = ds->class_rows_.find(make_pair(decc, format));
-       if (r != ds->class_rows_.end())
-               row_iter = ds->segments_.at(ds->current_segment_id_).annotation_rows.find((*r).second);
-       else {
-               // Failing that, use the decoder as a key
-               row_iter = ds->segments_.at(ds->current_segment_id_).annotation_rows.find(Row(0, decc));
+       // Find the row
+       Decoder* dec = ds->get_decoder_by_instance(srd_dec);
+       assert(dec);
+
+       AnnotationClass* ann_class = dec->get_ann_class_by_id(pda->ann_class);
+       if (!ann_class) {
+               qWarning() << "Decoder" << ds->display_name() << "wanted to add annotation" <<
+                       "with class ID" << pda->ann_class << "but there are only" <<
+                       dec->ann_classes().size() << "known classes";
+               return;
        }
 
-       if (row_iter == ds->segments_.at(ds->current_segment_id_).annotation_rows.end()) {
-               qDebug() << "Unexpected annotation: decoder = " << decc <<
-                       ", format = " << format;
-               assert(false);
+       const Row* row = ann_class->row;
+
+       if (!row)
+               row = dec->get_row_by_id(0);
+
+       // Add the annotation
+       ds->segments_[ds->current_segment_id_].annotation_rows.at(row).emplace_annotation(pdata);
+}
+
+void DecodeSignal::binary_callback(srd_proto_data *pdata, void *decode_signal)
+{
+       assert(pdata);
+       assert(decode_signal);
+
+       DecodeSignal *const ds = (DecodeSignal*)decode_signal;
+       assert(ds);
+
+       if (ds->decode_interrupt_)
+               return;
+
+       // Get the decoder and the binary data
+       assert(pdata->pdo);
+       assert(pdata->pdo->di);
+       const srd_decoder *const srd_dec = pdata->pdo->di->decoder;
+       assert(srd_dec);
+
+       const srd_proto_data_binary *const pdb = (const srd_proto_data_binary*)pdata->data;
+       assert(pdb);
+
+       // Find the matching DecodeBinaryClass
+       DecodeSegment* segment = &(ds->segments_.at(ds->current_segment_id_));
+
+       DecodeBinaryClass* bin_class = nullptr;
+       for (DecodeBinaryClass& bc : segment->binary_classes)
+               if ((bc.decoder->get_srd_decoder() == srd_dec) &&
+                       (bc.info->bin_class_id == (uint32_t)pdb->bin_class))
+                       bin_class = &bc;
+
+       if (!bin_class) {
+               qWarning() << "Could not find valid DecodeBinaryClass in segment" <<
+                               ds->current_segment_id_ << "for binary class ID" << pdb->bin_class <<
+                               ", segment only knows" << segment->binary_classes.size() << "classes";
                return;
        }
 
-       // Add the annotation
-       (*row_iter).second.emplace_annotation(pdata, &((*row_iter).first));
+       // Add the data chunk
+       bin_class->chunks.emplace_back();
+       DecodeBinaryDataChunk* chunk = &(bin_class->chunks.back());
+
+       chunk->sample = pdata->start_sample;
+       chunk->data.resize(pdb->size);
+       memcpy(chunk->data.data(), pdb->data, pdb->size);
+
+       Decoder* dec = ds->get_decoder_by_instance(srd_dec);
+
+       ds->new_binary_data(ds->current_segment_id_, (void*)dec, pdb->bin_class);
 }
 
 void DecodeSignal::on_capture_state_changed(int state)
index ba9c9b5e4ecaa8021b712446e487ede10a54eb6b..cee4ccf0eadcf6db1a214483057231e4274c7dbf 100644 (file)
@@ -21,6 +21,7 @@
 #define PULSEVIEW_PV_DATA_DECODESIGNAL_HPP
 
 #include <atomic>
+#include <deque>
 #include <condition_variable>
 #include <unordered_set>
 #include <vector>
@@ -30,6 +31,7 @@
 
 #include <libsigrokdecode/libsigrokdecode.h>
 
+#include <pv/data/decode/decoder.hpp>
 #include <pv/data/decode/row.hpp>
 #include <pv/data/decode/rowdata.hpp>
 #include <pv/data/signalbase.hpp>
 
 using std::atomic;
 using std::condition_variable;
+using std::deque;
 using std::map;
 using std::mutex;
-using std::pair;
 using std::vector;
 using std::shared_ptr;
 
+using pv::data::decode::Annotation;
+using pv::data::decode::DecodeBinaryClassInfo;
+using pv::data::decode::DecodeChannel;
+using pv::data::decode::Decoder;
+using pv::data::decode::Row;
+using pv::data::decode::RowData;
+
 namespace pv {
 class Session;
 
 namespace data {
 
-namespace decode {
-class Annotation;
-class Decoder;
-class Row;
-}
-
 class Logic;
 class LogicSegment;
 class SignalBase;
 class SignalData;
 
-struct DecodeChannel
+struct DecodeBinaryDataChunk
+{
+       vector<uint8_t> data;
+       uint64_t sample;   ///< Number of the sample where this data was provided by the PD
+};
+
+struct DecodeBinaryClass
 {
-       uint16_t id;     ///< Global numerical ID for the decode channels in the stack
-       uint16_t bit_id; ///< Tells which bit within a sample represents this channel
-       const bool is_optional;
-       const pv::data::SignalBase *assigned_signal;
-       const QString name, desc;
-       int initial_pin_state;
-       const shared_ptr<pv::data::decode::Decoder> decoder_;
-       const srd_channel *pdch_;
+       const Decoder* decoder;
+       const DecodeBinaryClassInfo* info;
+       deque<DecodeBinaryDataChunk> chunks;
 };
 
 struct DecodeSegment
 {
-       map<const decode::Row, decode::RowData> annotation_rows;
+       map<const Row*, RowData> annotation_rows;
        pv::util::Timestamp start_time;
        double samplerate;
        int64_t samples_decoded_incl, samples_decoded_excl;
+       vector<DecodeBinaryClass> binary_classes;
 };
 
 class DecodeSignal : public SignalBase
@@ -93,9 +98,9 @@ public:
        virtual ~DecodeSignal();
 
        bool is_decode_signal() const;
-       const vector< shared_ptr<data::decode::Decoder> >& decoder_stack() const;
+       const vector< shared_ptr<Decoder> >& decoder_stack() const;
 
-       void stack_decoder(const srd_decoder *decoder);
+       void stack_decoder(const srd_decoder *decoder, bool restart_decode=true);
        void remove_decoder(int index);
        bool toggle_decoder_visibility(int index);
 
@@ -106,8 +111,8 @@ public:
        bool is_paused() const;
        QString error_message() const;
 
-       const vector<data::DecodeChannel> get_channels() const;
-       void auto_assign_signals(const shared_ptr<pv::data::decode::Decoder> dec);
+       const vector<decode::DecodeChannel> get_channels() const;
+       void auto_assign_signals(const shared_ptr<Decoder> dec);
        void assign_signal(const uint16_t channel_id, const SignalBase *signal);
        int get_assigned_signal_count() const;
 
@@ -134,26 +139,42 @@ public:
        int64_t get_decoded_sample_count(uint32_t segment_id,
                bool include_processing) const;
 
-       vector<decode::Row> visible_rows() const;
+       vector<Row*> get_rows(bool visible_only=false);
+       vector<const Row*> get_rows(bool visible_only=false) const;
+
+       uint64_t get_annotation_count(const Row* row, uint32_t segment_id) const;
 
        /**
         * Extracts annotations from a single row into a vector.
         * Note: The annotations may be unsorted and only annotations that fully
         * fit into the sample range are considered.
         */
-       void get_annotation_subset(
-               vector<pv::data::decode::Annotation> &dest,
-               const decode::Row &row, uint32_t segment_id, uint64_t start_sample,
-               uint64_t end_sample) const;
+       void get_annotation_subset(deque<const Annotation*> &dest, const Row* row,
+               uint32_t segment_id, uint64_t start_sample, uint64_t end_sample) const;
 
        /**
         * Extracts annotations from all rows into a vector.
         * Note: The annotations may be unsorted and only annotations that fully
         * fit into the sample range are considered.
         */
-       void get_annotation_subset(
-               vector<pv::data::decode::Annotation> &dest,
-               uint32_t segment_id, uint64_t start_sample, uint64_t end_sample) const;
+       void get_annotation_subset(deque<const Annotation*> &dest, uint32_t segment_id,
+               uint64_t start_sample, uint64_t end_sample) const;
+
+       uint32_t get_binary_data_chunk_count(uint32_t segment_id,
+               const Decoder* dec, uint32_t bin_class_id) const;
+       void get_binary_data_chunk(uint32_t segment_id, const Decoder* dec,
+               uint32_t bin_class_id, uint32_t chunk_id, const vector<uint8_t> **dest,
+               uint64_t *size);
+       void get_merged_binary_data_chunks_by_sample(uint32_t segment_id,
+               const Decoder* dec, uint32_t bin_class_id,
+               uint64_t start_sample, uint64_t end_sample,
+               vector<uint8_t> *dest) const;
+       void get_merged_binary_data_chunks_by_offset(uint32_t segment_id,
+               const Decoder* dec, uint32_t bin_class_id,
+               uint64_t start, uint64_t end,
+               vector<uint8_t> *dest) const;
+       const DecodeBinaryClass* get_binary_data_class(uint32_t segment_id,
+               const Decoder* dec, uint32_t bin_class_id) const;
 
        virtual void save_settings(QSettings &settings) const;
 
@@ -163,20 +184,19 @@ private:
        void set_error_message(QString msg);
 
        uint32_t get_input_segment_count() const;
-
        uint32_t get_input_samplerate(uint32_t segment_id) const;
 
+       Decoder* get_decoder_by_instance(const srd_decoder *const srd_dec);
+
        void update_channel_list();
 
        void commit_decoder_channels();
 
        void mux_logic_samples(uint32_t segment_id, const int64_t start, const int64_t end);
-
        void logic_mux_proc();
 
        void decode_data(const int64_t abs_start_samplenum, const int64_t sample_count,
                const shared_ptr<LogicSegment> input_segment);
-
        void decode_proc();
 
        void start_srd_session();
@@ -188,9 +208,13 @@ private:
        void create_decode_segment();
 
        static void annotation_callback(srd_proto_data *pdata, void *decode_signal);
+       static void binary_callback(srd_proto_data *pdata, void *decode_signal);
 
 Q_SIGNALS:
+       void decoder_stacked(void* decoder); ///< decoder is of type decode::Decoder*
+       void decoder_removed(void* decoder); ///< decoder is of type decode::Decoder*
        void new_annotations(); // TODO Supply segment for which they belong to
+       void new_binary_data(unsigned int segment_id, void* decoder, unsigned int bin_class_id);
        void decode_reset();
        void decode_finished();
        void channels_updated();
@@ -203,7 +227,7 @@ private Q_SLOTS:
 private:
        pv::Session &session_;
 
-       vector<data::DecodeChannel> channels_;
+       vector<decode::DecodeChannel> channels_;
 
        struct srd_session *srd_session_;
 
@@ -211,9 +235,8 @@ private:
        uint32_t logic_mux_unit_size_;
        bool logic_mux_data_invalid_;
 
-       vector< shared_ptr<decode::Decoder> > stack_;
+       vector< shared_ptr<Decoder> > stack_;
        bool stack_config_changed_;
-       map<pair<const srd_decoder*, int>, decode::Row> class_rows_;
 
        vector<DecodeSegment> segments_;
        uint32_t current_segment_id_;
index 4170f6422d521b0a519432a04b680423844eaf73..b9e57caa9e797725ed5532e226353cef527b7649 100644 (file)
@@ -349,7 +349,7 @@ void LogicSegment::append_payload(void *data, uint64_t data_size)
 }
 
 void LogicSegment::get_samples(int64_t start_sample,
-       int64_t end_sample,     uint8_t* dest) const
+       int64_t end_sample, uint8_t* dest) const
 {
        assert(start_sample >= 0);
        assert(start_sample <= (int64_t)sample_count_);
index 78633de977706ac5947969f1138fb340562a170a..fde99e2b4619a3cadf2b2acd01a862759c183d3f 100644 (file)
@@ -83,7 +83,7 @@ QString SignalBase::internal_name() const
 
 QString SignalBase::display_name() const
 {
-       if (name() != internal_name_)
+       if ((name() != internal_name_) && (!internal_name_.isEmpty()))
                return name() + " (" + internal_name_ + ")";
        else
                return name();
index 5090b480e3d49adc41bc4e861102ce9a6e6bbb8b..09d9014bddf02602dd44018ee3ce325617ec3e2a 100644 (file)
@@ -93,7 +93,7 @@ DeviceManager::DeviceManager(shared_ptr<Context> context,
                if (!driver_supported(entry.second))
                        continue;
 
-               progress->setLabelText(QObject::tr("Scanning for %1...")
+               progress->setLabelText(QObject::tr("Scanning for devices that driver %1 can access...")
                        .arg(QString::fromStdString(entry.first)));
 
                if (entry.first == user_name)
index 084b1142c198c2b24b82ae3da4a00590f3d042c2..d238d681097bc5fcfd87c2351e87949f09dc16de 100644 (file)
@@ -82,8 +82,24 @@ Connect::Connect(QWidget *parent, pv::DeviceManager &device_manager) :
 
        radiobtn_usb->setChecked(true);
 
+       serial_config_ = new QWidget();
+       QHBoxLayout *serial_config_layout = new QHBoxLayout(serial_config_);
+
        serial_devices_.setEditable(true);
-       serial_devices_.setEnabled(false);
+       serial_devices_.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+
+       serial_baudrate_.setEditable(true);
+       serial_baudrate_.addItem("");
+       serial_baudrate_.addItem("921600");
+       serial_baudrate_.addItem("115200");
+       serial_baudrate_.addItem("57600");
+       serial_baudrate_.addItem("19200");
+       serial_baudrate_.addItem("9600");
+
+       serial_config_layout->addWidget(&serial_devices_);
+       serial_config_layout->addWidget(&serial_baudrate_);
+       serial_config_layout->addWidget(new QLabel("baud"));
+       serial_config_->setEnabled(false);
 
        tcp_config_ = new QWidget();
        QHBoxLayout *tcp_config_layout = new QHBoxLayout(tcp_config_);
@@ -111,7 +127,7 @@ Connect::Connect(QWidget *parent, pv::DeviceManager &device_manager) :
        QVBoxLayout *vbox_if = new QVBoxLayout;
        vbox_if->addWidget(radiobtn_usb);
        vbox_if->addWidget(radiobtn_serial);
-       vbox_if->addWidget(&serial_devices_);
+       vbox_if->addWidget(serial_config_);
        vbox_if->addWidget(radiobtn_tcp);
        vbox_if->addWidget(tcp_config_);
 
@@ -194,6 +210,8 @@ void Connect::unset_connection()
 void Connect::serial_toggled(bool checked)
 {
        serial_devices_.setEnabled(checked);
+       serial_baudrate_.setEnabled(checked);
+       serial_config_->setEnabled(checked);
 }
 
 void Connect::tcp_toggled(bool checked)
@@ -216,7 +234,7 @@ void Connect::scan_pressed()
 
        map<const ConfigKey *, VariantBase> drvopts;
 
-       if (serial_devices_.isEnabled()) {
+       if (serial_config_->isEnabled()) {
                QString serial;
                const int index = serial_devices_.currentIndex();
                if (index >= 0 && index < serial_devices_.count() &&
@@ -224,8 +242,14 @@ void Connect::scan_pressed()
                        serial = serial_devices_.itemData(index).toString();
                else
                        serial = serial_devices_.currentText();
+
                drvopts[ConfigKey::CONN] = Variant<ustring>::create(
                        serial.toUtf8().constData());
+
+               // Set baud rate if specified
+               if (serial_baudrate_.currentText().length() > 0)
+                       drvopts[ConfigKey::SERIALCOMM] = Variant<ustring>::create(
+                               QString("%1/8n1").arg(serial_baudrate_.currentText()).toUtf8().constData());
        }
 
        if (tcp_config_->isEnabled()) {
index 914c58783ee3693b87ed6b7c2a3232c0eb119a62..f34268ddb6fde5a383cd56e42dd165da04b497e7 100644 (file)
@@ -89,7 +89,8 @@ private:
 
        QComboBox drivers_;
 
-       QComboBox serial_devices_;
+       QWidget *serial_config_;
+       QComboBox serial_devices_, serial_baudrate_;
 
        QWidget *tcp_config_;
        QLineEdit *tcp_host_;
index 2bbab122ffb3731159f2385f8c6457150ee0d1a2..9f458c99953e8c26c1721b5b9e819e73f9f1a1f7 100644 (file)
@@ -204,6 +204,7 @@ QPlainTextEdit *Settings::create_log_view() const
 QWidget *Settings::get_general_settings_form(QWidget *parent) const
 {
        GlobalSettings settings;
+       QCheckBox *cb;
 
        QWidget *form = new QWidget(parent);
        QVBoxLayout *form_layout = new QVBoxLayout(form);
@@ -215,6 +216,26 @@ QWidget *Settings::get_general_settings_form(QWidget *parent) const
        QFormLayout *general_layout = new QFormLayout();
        general_group->setLayout(general_layout);
 
+       // Generate language combobox
+       QComboBox *language_cb = new QComboBox();
+       Application* a = qobject_cast<Application*>(QApplication::instance());
+
+       QString current_language = settings.value(GlobalSettings::Key_General_Language).toString();
+       for (const QString& language : a->get_languages()) {
+               QLocale locale = QLocale(language);
+               QString desc = locale.languageToString(locale.language());
+               language_cb->addItem(desc, language);
+
+               if (language == current_language) {
+                       int index = language_cb->findText(desc, Qt::MatchFixedString);
+                       language_cb->setCurrentIndex(index);
+               }
+       }
+       connect(language_cb, SIGNAL(currentIndexChanged(const QString&)),
+               this, SLOT(on_general_language_changed(const QString&)));
+       general_layout->addRow(tr("User interface language"), language_cb);
+
+       // Theme combobox
        QComboBox *theme_cb = new QComboBox();
        for (const pair<QString, QString>& entry : Themes)
                theme_cb->addItem(entry.first, entry.second);
@@ -222,13 +243,14 @@ QWidget *Settings::get_general_settings_form(QWidget *parent) const
        theme_cb->setCurrentIndex(
                settings.value(GlobalSettings::Key_General_Theme).toInt());
        connect(theme_cb, SIGNAL(currentIndexChanged(int)),
-               this, SLOT(on_general_theme_changed_changed(int)));
+               this, SLOT(on_general_theme_changed(int)));
        general_layout->addRow(tr("User interface theme"), theme_cb);
 
        QLabel *description_1 = new QLabel(tr("(You may need to restart PulseView for all UI elements to update)"));
        description_1->setAlignment(Qt::AlignRight);
        general_layout->addRow(description_1);
 
+       // Style combobox
        QComboBox *style_cb = new QComboBox();
        style_cb->addItem(tr("System Default"), "");
        for (QString& s : QStyleFactory::keys())
@@ -239,7 +261,7 @@ QWidget *Settings::get_general_settings_form(QWidget *parent) const
        if (current_style.isEmpty())
                style_cb->setCurrentIndex(0);
        else
-               style_cb->setCurrentIndex(style_cb->findText(current_style, 0));
+               style_cb->setCurrentIndex(style_cb->findText(current_style, nullptr));
 
        connect(style_cb, SIGNAL(currentIndexChanged(int)),
                this, SLOT(on_general_style_changed(int)));
@@ -249,6 +271,11 @@ QWidget *Settings::get_general_settings_form(QWidget *parent) const
        description_2->setAlignment(Qt::AlignRight);
        general_layout->addRow(description_2);
 
+       // Misc
+       cb = create_checkbox(GlobalSettings::Key_General_SaveWithSetup,
+               SLOT(on_general_save_with_setup_changed(int)));
+       general_layout->addRow(tr("Save session &setup along with .sr file"), cb);
+
        return form;
 }
 
@@ -317,7 +344,7 @@ QWidget *Settings::get_view_settings_form(QWidget *parent) const
                settings.value(GlobalSettings::Key_View_SnapDistance).toInt());
        connect(snap_distance_sb, SIGNAL(valueChanged(int)), this,
                SLOT(on_view_snapDistance_changed(int)));
-       trace_view_layout->addRow(tr("Maximum distance from edges before cursors snap to them"), snap_distance_sb);
+       trace_view_layout->addRow(tr("Maximum distance from edges before markers snap to them"), snap_distance_sb);
 
        ColorButton* cursor_fill_cb = new ColorButton(parent);
        cursor_fill_cb->set_color(QColor::fromRgba(
@@ -377,6 +404,10 @@ QWidget *Settings::get_decoder_settings_form(QWidget *parent)
                SLOT(on_dec_initialStateConfigurable_changed(int)));
        decoder_layout->addRow(tr("Allow configuration of &initial signal state"), cb);
 
+       cb = create_checkbox(GlobalSettings::Key_Dec_AlwaysShowAllRows,
+               SLOT(on_dec_alwaysshowallrows_changed(int)));
+       decoder_layout->addRow(tr("Always show all &rows, even if no annotation is visible"), cb);
+
        // Annotation export settings
        ann_export_format_ = new QLineEdit();
        ann_export_format_->setText(
@@ -384,10 +415,10 @@ QWidget *Settings::get_decoder_settings_form(QWidget *parent)
        connect(ann_export_format_, SIGNAL(textChanged(const QString&)),
                this, SLOT(on_dec_exportFormat_changed(const QString&)));
        decoder_layout->addRow(tr("Annotation export format"), ann_export_format_);
-       QLabel *description_1 = new QLabel(tr("%s = sample range; %d: decoder name; %c: row name; %q: use quotations marks"));
+       QLabel *description_1 = new QLabel(tr("%s = sample range; %d: decoder name; %r: row name; %c: class name"));
        description_1->setAlignment(Qt::AlignRight);
        decoder_layout->addRow(description_1);
-       QLabel *description_2 = new QLabel(tr("%1: longest annotation text; %a: all annotation texts"));
+       QLabel *description_2 = new QLabel(tr("%1: longest annotation text; %a: all annotation texts; %q: use quotation marks"));
        description_2->setAlignment(Qt::AlignRight);
        decoder_layout->addRow(description_2);
 
@@ -574,10 +605,24 @@ void Settings::on_page_changed(QListWidgetItem *current, QListWidgetItem *previo
        pages->setCurrentIndex(page_list->row(current));
 }
 
-void Settings::on_general_theme_changed_changed(int state)
+void Settings::on_general_language_changed(const QString &text)
 {
        GlobalSettings settings;
-       settings.setValue(GlobalSettings::Key_General_Theme, state);
+       Application* a = qobject_cast<Application*>(QApplication::instance());
+
+       for (const QString& language : a->get_languages()) {
+               QLocale locale = QLocale(language);
+               QString desc = locale.languageToString(locale.language());
+
+               if (text == desc)
+                       settings.setValue(GlobalSettings::Key_General_Language, language);
+       }
+}
+
+void Settings::on_general_theme_changed(int value)
+{
+       GlobalSettings settings;
+       settings.setValue(GlobalSettings::Key_General_Theme, value);
        settings.apply_theme();
 
        QMessageBox msg(this);
@@ -599,19 +644,25 @@ void Settings::on_general_theme_changed_changed(int state)
        }
 }
 
-void Settings::on_general_style_changed(int state)
+void Settings::on_general_style_changed(int value)
 {
        GlobalSettings settings;
 
-       if (state == 0)
+       if (value == 0)
                settings.setValue(GlobalSettings::Key_General_Style, "");
        else
                settings.setValue(GlobalSettings::Key_General_Style,
-                       QStyleFactory::keys().at(state - 1));
+                       QStyleFactory::keys().at(value - 1));
 
        settings.apply_theme();
 }
 
+void Settings::on_general_save_with_setup_changed(int state)
+{
+       GlobalSettings settings;
+       settings.setValue(GlobalSettings::Key_General_SaveWithSetup, state ? true : false);
+}
+
 void Settings::on_view_zoomToFitDuringAcq_changed(int state)
 {
        GlobalSettings settings;
@@ -714,6 +765,12 @@ void Settings::on_dec_exportFormat_changed(const QString &text)
        GlobalSettings settings;
        settings.setValue(GlobalSettings::Key_Dec_ExportFormat, text);
 }
+
+void Settings::on_dec_alwaysshowallrows_changed(int state)
+{
+       GlobalSettings settings;
+       settings.setValue(GlobalSettings::Key_Dec_AlwaysShowAllRows, state ? true : false);
+}
 #endif
 
 void Settings::on_log_logLevel_changed(int value)
@@ -744,8 +801,7 @@ void Settings::on_log_saveToFile_clicked(bool checked)
 
                if (out_stream.status() == QTextStream::Ok) {
                        QMessageBox msg(this);
-                       msg.setText(tr("Success"));
-                       msg.setInformativeText(tr("Log saved to %1.").arg(file_name));
+                       msg.setText(tr("Success") + "\n\n" + tr("Log saved to %1.").arg(file_name));
                        msg.setStandardButtons(QMessageBox::Ok);
                        msg.setIcon(QMessageBox::Information);
                        msg.exec();
@@ -755,8 +811,7 @@ void Settings::on_log_saveToFile_clicked(bool checked)
        }
 
        QMessageBox msg(this);
-       msg.setText(tr("Error"));
-       msg.setInformativeText(tr("File %1 could not be written to.").arg(file_name));
+       msg.setText(tr("Error") + "\n\n" + tr("File %1 could not be written to.").arg(file_name));
        msg.setStandardButtons(QMessageBox::Ok);
        msg.setIcon(QMessageBox::Warning);
        msg.exec();
index 43988fec9e09b2889237dd7a97dfa86d3558be6e..50f3be65c51fe9fb97569f917034c501c85f5220 100644 (file)
@@ -58,8 +58,10 @@ public:
 
 private Q_SLOTS:
        void on_page_changed(QListWidgetItem *current, QListWidgetItem *previous);
-       void on_general_theme_changed_changed(int state);
-       void on_general_style_changed(int state);
+       void on_general_language_changed(const QString &text);
+       void on_general_theme_changed(int value);
+       void on_general_style_changed(int value);
+       void on_general_save_with_setup_changed(int state);
        void on_view_zoomToFitDuringAcq_changed(int state);
        void on_view_zoomToFitAfterAcq_changed(int state);
        void on_view_triggerIsZero_changed(int state);
@@ -78,6 +80,7 @@ private Q_SLOTS:
 #ifdef ENABLE_DECODE
        void on_dec_initialStateConfigurable_changed(int state);
        void on_dec_exportFormat_changed(const QString &text);
+       void on_dec_alwaysshowallrows_changed(int state);
 #endif
        void on_log_logLevel_changed(int value);
        void on_log_bufferSize_changed(int value);
index 9f4279cea8557a20423c799f0f6abe5785e142ee..2bca3476c434b53ff65e99b4774145dbff248592 100644 (file)
@@ -82,8 +82,7 @@ void StoreProgress::show_error()
        qDebug() << "Error trying to save:" << session_.error();
 
        QMessageBox msg(parentWidget());
-       msg.setText(tr("Failed to save session."));
-       msg.setInformativeText(session_.error());
+       msg.setText(tr("Failed to save session.") + "\n\n" + session_.error());
        msg.setStandardButtons(QMessageBox::Ok);
        msg.setIcon(QMessageBox::Warning);
        msg.exec();
index 34dbc8326e8acb0f5db01d4c195764af310c73ab..ca649befad8f318dab49f90e2ef73ad766756625 100644 (file)
@@ -17,7 +17,9 @@
  * along with this program; if not, see <http://www.gnu.org/licenses/>.
  */
 
-#include "globalsettings.hpp"
+#include <boost/archive/text_iarchive.hpp>
+#include <boost/archive/text_oarchive.hpp>
+#include <boost/serialization/serialization.hpp>
 
 #include <QApplication>
 #include <QColor>
 #include <QStyle>
 #include <QtGlobal>
 
+#include "globalsettings.hpp"
+#include "application.hpp"
+
 using std::map;
 using std::pair;
 using std::string;
+using std::stringstream;
 using std::vector;
 
 namespace pv {
@@ -42,8 +48,10 @@ const vector< pair<QString, QString> > Themes {
        {"DarkStyle", ":/themes/darkstyle/darkstyle.qss"}
 };
 
+const QString GlobalSettings::Key_General_Language = "General_Language";
 const QString GlobalSettings::Key_General_Theme = "General_Theme";
 const QString GlobalSettings::Key_General_Style = "General_Style";
+const QString GlobalSettings::Key_General_SaveWithSetup = "General_SaveWithSetup";
 const QString GlobalSettings::Key_View_ZoomToFitDuringAcq = "View_ZoomToFitDuringAcq";
 const QString GlobalSettings::Key_View_ZoomToFitAfterAcq = "View_ZoomToFitAfterAcq";
 const QString GlobalSettings::Key_View_TriggerIsZeroTime = "View_TriggerIsZeroTime";
@@ -59,20 +67,24 @@ const QString GlobalSettings::Key_View_DefaultLogicHeight = "View_DefaultLogicHe
 const QString GlobalSettings::Key_View_ShowHoverMarker = "View_ShowHoverMarker";
 const QString GlobalSettings::Key_View_SnapDistance = "View_SnapDistance";
 const QString GlobalSettings::Key_View_CursorFillColor = "View_CursorFillColor";
+const QString GlobalSettings::Key_View_CursorShowFrequency = "View_CursorShowFrequency";
+const QString GlobalSettings::Key_View_CursorShowInterval = "View_CursorShowInterval";
+const QString GlobalSettings::Key_View_CursorShowSamples = "View_CursorShowSamples";
 const QString GlobalSettings::Key_Dec_InitialStateConfigurable = "Dec_InitialStateConfigurable";
 const QString GlobalSettings::Key_Dec_ExportFormat = "Dec_ExportFormat";
+const QString GlobalSettings::Key_Dec_AlwaysShowAllRows = "Dec_AlwaysShowAllRows";
 const QString GlobalSettings::Key_Log_BufferSize = "Log_BufferSize";
 const QString GlobalSettings::Key_Log_NotifyOfStacktrace = "Log_NotifyOfStacktrace";
 
 vector<GlobalSettingsInterface*> GlobalSettings::callbacks_;
 bool GlobalSettings::tracking_ = false;
+bool GlobalSettings::is_dark_theme_ = false;
 map<QString, QVariant> GlobalSettings::tracked_changes_;
 QString GlobalSettings::default_style_;
 QPalette GlobalSettings::default_palette_;
 
 GlobalSettings::GlobalSettings() :
-       QSettings(),
-       is_dark_theme_(false)
+       QSettings()
 {
        beginGroup("Settings");
 }
@@ -88,12 +100,25 @@ void GlobalSettings::save_internal_defaults()
 
 void GlobalSettings::set_defaults_where_needed()
 {
+       if (!contains(Key_General_Language)) {
+               // Determine and set default UI language
+               QString language = QLocale().uiLanguages().first();  // May return e.g. en-Latn-US  // clazy:exclude=detaching-temporary
+               language = language.split("-").first();
+
+               setValue(Key_General_Language, language);
+               apply_language();
+       }
+
        // Use no theme by default
        if (!contains(Key_General_Theme))
                setValue(Key_General_Theme, 0);
        if (!contains(Key_General_Style))
                setValue(Key_General_Style, "");
 
+       // Save setup with .sr files by default
+       if (!contains(Key_General_SaveWithSetup))
+               setValue(Key_General_SaveWithSetup, true);
+
        // Enable zoom-to-fit after acquisition by default
        if (!contains(Key_View_ZoomToFitAfterAcq))
                setValue(Key_View_ZoomToFitAfterAcq, true);
@@ -124,8 +149,16 @@ void GlobalSettings::set_defaults_where_needed()
        if (!contains(Key_View_SnapDistance))
                setValue(Key_View_SnapDistance, 15);
 
-       if (!contains(Key_Dec_ExportFormat))
-               setValue(Key_Dec_ExportFormat, "%s %d: %c: %1");
+       if (!contains(Key_View_CursorShowInterval))
+               setValue(Key_View_CursorShowInterval, true);
+
+       if (!contains(Key_View_CursorShowFrequency))
+               setValue(Key_View_CursorShowFrequency, true);
+
+       // %c was used for the row name in the past so we need to transition such users
+       if (!contains(Key_Dec_ExportFormat) ||
+               value(Key_Dec_ExportFormat).toString() == "%s %d: %c: %1")
+               setValue(Key_Dec_ExportFormat, "%s %d: %r: %1");
 
        // Default to 500 lines of backlog
        if (!contains(Key_Log_BufferSize))
@@ -223,6 +256,12 @@ void GlobalSettings::apply_theme()
        QPixmapCache::clear();
 }
 
+void GlobalSettings::apply_language()
+{
+       Application* a = qobject_cast<Application*>(QApplication::instance());
+       a->switch_language(value(Key_General_Language).toString());
+}
+
 void GlobalSettings::add_change_handler(GlobalSettingsInterface *cb)
 {
        callbacks_.push_back(cb);
@@ -337,4 +376,28 @@ Glib::VariantBase GlobalSettings::restore_variantbase(QSettings &settings)
        return ret_val;
 }
 
+void GlobalSettings::store_timestamp(QSettings &settings, const char *name, const pv::util::Timestamp &ts)
+{
+       stringstream ss;
+       boost::archive::text_oarchive oa(ss);
+       oa << boost::serialization::make_nvp(name, ts);
+       settings.setValue(name, QString::fromStdString(ss.str()));
+}
+
+pv::util::Timestamp GlobalSettings::restore_timestamp(QSettings &settings, const char *name)
+{
+       util::Timestamp result;
+       stringstream ss;
+       ss << settings.value(name).toString().toStdString();
+
+       try {
+               boost::archive::text_iarchive ia(ss);
+               ia >> boost::serialization::make_nvp(name, result);
+       } catch (boost::archive::archive_exception&) {
+               qDebug() << "Could not restore setting" << name;
+       }
+
+       return result;
+}
+
 } // namespace pv
index e890a800e306b302f0b5939c28d7c78bcfe537d0..cf1921e89ea52b551f3ca46a85102b04a9dbf502 100644 (file)
@@ -30,6 +30,8 @@
 #include <QString>
 #include <QVariant>
 
+#include "util.hpp"
+
 using std::map;
 using std::pair;
 using std::vector;
@@ -51,8 +53,10 @@ class GlobalSettings : public QSettings
        Q_OBJECT
 
 public:
+       static const QString Key_General_Language;
        static const QString Key_General_Theme;
        static const QString Key_General_Style;
+       static const QString Key_General_SaveWithSetup;
        static const QString Key_View_ZoomToFitDuringAcq;
        static const QString Key_View_ZoomToFitAfterAcq;
        static const QString Key_View_TriggerIsZeroTime;
@@ -68,8 +72,12 @@ public:
        static const QString Key_View_ShowHoverMarker;
        static const QString Key_View_SnapDistance;
        static const QString Key_View_CursorFillColor;
+       static const QString Key_View_CursorShowInterval;
+       static const QString Key_View_CursorShowFrequency;
+       static const QString Key_View_CursorShowSamples;
        static const QString Key_Dec_InitialStateConfigurable;
        static const QString Key_Dec_ExportFormat;
+       static const QString Key_Dec_AlwaysShowAllRows;
        static const QString Key_Log_BufferSize;
        static const QString Key_Log_NotifyOfStacktrace;
 
@@ -87,9 +95,11 @@ public:
        void set_bright_theme_default_colors();
        void set_dark_theme_default_colors();
 
-       bool current_theme_is_dark();
+       static bool current_theme_is_dark();
        void apply_theme();
 
+       void apply_language();
+
        static void add_change_handler(GlobalSettingsInterface *cb);
        static void remove_change_handler(GlobalSettingsInterface *cb);
 
@@ -114,13 +124,14 @@ public:
        void undo_tracked_changes();
 
        static void store_gvariant(QSettings &settings, GVariant *v);
-
        static GVariant* restore_gvariant(QSettings &settings);
 
        static void store_variantbase(QSettings &settings, Glib::VariantBase v);
-
        static Glib::VariantBase restore_variantbase(QSettings &settings);
 
+       static void store_timestamp(QSettings &settings, const char *name, const pv::util::Timestamp &ts);
+       static pv::util::Timestamp restore_timestamp(QSettings &settings, const char *name);
+
 private:
        static vector<GlobalSettingsInterface*> callbacks_;
 
@@ -130,7 +141,7 @@ private:
        static QString default_style_;
        static QPalette default_palette_;
 
-       bool is_dark_theme_;
+       static bool is_dark_theme_;
 };
 
 } // namespace pv
index b747ab5452c5f4ee7b2aef633a65fc1e02986f47..e42a9e54e94add49cb46b34f3a5ab216650e6c3f 100644 (file)
@@ -189,7 +189,10 @@ int Logging::log_srd(void *cb_data, int loglevel, const char *format, va_list ar
        va_end(args2);
 
        char *text = g_strdup_vprintf(format, args);
-       logging.log(QString::fromUtf8(text), LogSource_srd);
+
+       QString s = QString::fromUtf8(text);
+       for (QString& substring : s.split("\n", QString::SkipEmptyParts))
+                       logging.log(substring, LogSource_srd);
        g_free(text);
 
        return SR_OK;
index fd889e9e9176d6017c340f30d31d4b2b105b545d..ea9f01af7fd13f6ca056ef7dfe37333b158cbb9d 100644 (file)
@@ -40,6 +40,7 @@
 
 #include "mainwindow.hpp"
 
+#include "application.hpp"
 #include "devicemanager.hpp"
 #include "devices/hardwaredevice.hpp"
 #include "dialogs/settings.hpp"
 #include "views/trace/view.hpp"
 #include "views/trace/standardbar.hpp"
 
+#ifdef ENABLE_DECODE
+#include "subwindows/decoder_selector/subwindow.hpp"
+#include "views/decoder_binary/view.hpp"
+#endif
+
 #include <libsigrokcxx/libsigrokcxx.hpp>
 
 using std::dynamic_pointer_cast;
@@ -58,10 +64,6 @@ using std::string;
 
 namespace pv {
 
-namespace view {
-class ViewItem;
-}
-
 using toolbars::MainBar;
 
 const QString MainWindow::WindowTitle = tr("PulseView");
@@ -70,23 +72,23 @@ MainWindow::MainWindow(DeviceManager &device_manager, QWidget *parent) :
        QMainWindow(parent),
        device_manager_(device_manager),
        session_selector_(this),
-       session_state_mapper_(this),
        icon_red_(":/icons/status-red.svg"),
        icon_green_(":/icons/status-green.svg"),
        icon_grey_(":/icons/status-grey.svg")
 {
-       GlobalSettings::add_change_handler(this);
-
        setup_ui();
        restore_ui_settings();
 }
 
 MainWindow::~MainWindow()
 {
-       GlobalSettings::remove_change_handler(this);
+       // Make sure we no longer hold any shared pointers to widgets after the
+       // destructor finishes (goes for sessions and sub windows alike)
 
        while (!sessions_.empty())
                remove_session(sessions_.front());
+
+       sub_windows_.clear();
 }
 
 void MainWindow::show_session_error(const QString text, const QString info_text)
@@ -95,8 +97,7 @@ void MainWindow::show_session_error(const QString text, const QString info_text)
        qDebug() << "Notifying user of session error:" << info_text;
 
        QMessageBox msg;
-       msg.setText(text);
-       msg.setInformativeText(info_text);
+       msg.setText(text + "\n\n" + info_text);
        msg.setStandardButtons(QMessageBox::Ok);
        msg.setIcon(QMessageBox::Warning);
        msg.exec();
@@ -127,8 +128,8 @@ shared_ptr<views::ViewBase> MainWindow::get_active_view() const
        return nullptr;
 }
 
-shared_ptr<views::ViewBase> MainWindow::add_view(const QString &title,
-       views::ViewType type, Session &session)
+shared_ptr<views::ViewBase> MainWindow::add_view(views::ViewType type,
+       Session &session)
 {
        GlobalSettings settings;
        shared_ptr<views::ViewBase> v;
@@ -142,6 +143,13 @@ shared_ptr<views::ViewBase> MainWindow::add_view(const QString &title,
 
        shared_ptr<MainBar> main_bar = session.main_bar();
 
+       // Only use the view type in the name if it's not the main view
+       QString title;
+       if (main_bar)
+               title = QString("%1 (%2)").arg(session.name(), views::ViewTypeNames[type]);
+       else
+               title = session.name();
+
        QDockWidget* dock = new QDockWidget(title, main_window);
        dock->setObjectName(title);
        main_window->addDockWidget(Qt::TopDockWidgetArea, dock);
@@ -152,8 +160,11 @@ shared_ptr<views::ViewBase> MainWindow::add_view(const QString &title,
 
        if (type == views::ViewTypeTrace)
                // This view will be the main view if there's no main bar yet
-               v = make_shared<views::trace::View>(session,
-                       (main_bar ? false : true), dock_main);
+               v = make_shared<views::trace::View>(session, (main_bar ? false : true), dock_main);
+#ifdef ENABLE_DECODE
+       if (type == views::ViewTypeDecoderBinary)
+               v = make_shared<views::decoder_binary::View>(session, false, dock_main);
+#endif
 
        if (!v)
                return nullptr;
@@ -183,18 +194,16 @@ shared_ptr<views::ViewBase> MainWindow::add_view(const QString &title,
                views::trace::View *tv =
                        qobject_cast<views::trace::View*>(v.get());
 
-               tv->enable_colored_bg(settings.value(GlobalSettings::Key_View_ColoredBG).toBool());
-               tv->enable_show_sampling_points(settings.value(GlobalSettings::Key_View_ShowSamplingPoints).toBool());
-               tv->enable_show_analog_minor_grid(settings.value(GlobalSettings::Key_View_ShowAnalogMinorGrid).toBool());
-
                if (!main_bar) {
                        /* Initial view, create the main bar */
                        main_bar = make_shared<MainBar>(session, this, tv);
                        dock_main->addToolBar(main_bar.get());
                        session.set_main_bar(main_bar);
 
-                       connect(main_bar.get(), SIGNAL(new_view(Session*)),
-                               this, SLOT(on_new_view(Session*)));
+                       connect(main_bar.get(), SIGNAL(new_view(Session*, int)),
+                               this, SLOT(on_new_view(Session*, int)));
+                       connect(main_bar.get(), SIGNAL(show_decoder_selector(Session*)),
+                               this, SLOT(on_show_decoder_selector(Session*)));
 
                        main_bar->action_view_show_cursors()->setChecked(tv->cursors_shown());
 
@@ -213,6 +222,8 @@ shared_ptr<views::ViewBase> MainWindow::add_view(const QString &title,
                }
        }
 
+       v->setFocus();
+
        return v;
 }
 
@@ -244,6 +255,74 @@ void MainWindow::remove_view(shared_ptr<views::ViewBase> view)
        }
 }
 
+shared_ptr<subwindows::SubWindowBase> MainWindow::add_subwindow(
+       subwindows::SubWindowType type, Session &session)
+{
+       GlobalSettings settings;
+       shared_ptr<subwindows::SubWindowBase> w;
+
+       QMainWindow *main_window = nullptr;
+       for (auto& entry : session_windows_)
+               if (entry.first.get() == &session)
+                       main_window = entry.second;
+
+       assert(main_window);
+
+       QString title = "";
+
+       switch (type) {
+#ifdef ENABLE_DECODE
+               case subwindows::SubWindowTypeDecoderSelector:
+                       title = tr("Decoder Selector");
+                       break;
+#endif
+               default:
+                       break;
+       }
+
+       QDockWidget* dock = new QDockWidget(title, main_window);
+       dock->setObjectName(title);
+       main_window->addDockWidget(Qt::TopDockWidgetArea, dock);
+
+       // Insert a QMainWindow into the dock widget to allow for a tool bar
+       QMainWindow *dock_main = new QMainWindow(dock);
+       dock_main->setWindowFlags(Qt::Widget);  // Remove Qt::Window flag
+
+#ifdef ENABLE_DECODE
+       if (type == subwindows::SubWindowTypeDecoderSelector)
+               w = make_shared<subwindows::decoder_selector::SubWindow>(session, dock_main);
+#endif
+
+       if (!w)
+               return nullptr;
+
+       sub_windows_[dock] = w;
+       dock_main->setCentralWidget(w.get());
+       dock->setWidget(dock_main);
+
+       dock->setContextMenuPolicy(Qt::PreventContextMenu);
+       dock->setFeatures(QDockWidget::DockWidgetMovable |
+               QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable);
+
+       QAbstractButton *close_btn =
+               dock->findChildren<QAbstractButton*>  // clazy:exclude=detaching-temporary
+                       ("qt_dockwidget_closebutton").front();
+
+       // Allow all subwindows to be closed via ESC.
+       close_btn->setShortcut(QKeySequence(Qt::Key_Escape));
+
+       connect(close_btn, SIGNAL(clicked(bool)),
+               this, SLOT(on_sub_window_close_clicked()));
+
+       if (w->has_toolbar())
+               dock_main->addToolBar(w->create_toolbar(dock_main));
+
+       if (w->minimum_width() > 0)
+               dock->setMinimumSize(w->minimum_width(), 0);
+
+       return w;
+}
+
 shared_ptr<Session> MainWindow::add_session()
 {
        static int last_session_id = 1;
@@ -251,13 +330,14 @@ shared_ptr<Session> MainWindow::add_session()
 
        shared_ptr<Session> session = make_shared<Session>(device_manager_, name);
 
-       connect(session.get(), SIGNAL(add_view(const QString&, views::ViewType, Session*)),
-               this, SLOT(on_add_view(const QString&, views::ViewType, Session*)));
+       connect(session.get(), SIGNAL(add_view(views::ViewType, Session*)),
+               this, SLOT(on_add_view(views::ViewType, Session*)));
        connect(session.get(), SIGNAL(name_changed()),
                this, SLOT(on_session_name_changed()));
-       session_state_mapper_.setMapping(session.get(), session.get());
+       connect(session.get(), SIGNAL(device_changed()),
+               this, SLOT(on_session_device_changed()));
        connect(session.get(), SIGNAL(capture_state_changed(int)),
-               &session_state_mapper_, SLOT(map()));
+               this, SLOT(on_session_capture_state_changed(int)));
 
        sessions_.push_back(session);
 
@@ -271,8 +351,7 @@ shared_ptr<Session> MainWindow::add_session()
 
        window->setDockNestingEnabled(true);
 
-       shared_ptr<views::ViewBase> main_view =
-               add_view(name, views::ViewTypeTrace, *session);
+       add_view(views::ViewTypeTrace, *session);
 
        return session;
 }
@@ -324,10 +403,10 @@ void MainWindow::remove_session(shared_ptr<Session> session)
 }
 
 void MainWindow::add_session_with_file(string open_file_name,
-       string open_file_format)
+       string open_file_format, string open_setup_file_name)
 {
        shared_ptr<Session> session = add_session();
-       session->load_init_file(open_file_name, open_file_format);
+       session->load_init_file(open_file_name, open_file_format, open_setup_file_name);
 }
 
 void MainWindow::add_default_session()
@@ -401,18 +480,6 @@ void MainWindow::restore_sessions()
        }
 }
 
-void MainWindow::on_setting_changed(const QString &key, const QVariant &value)
-{
-       if (key == GlobalSettings::Key_View_ColoredBG)
-               on_settingViewColoredBg_changed(value);
-
-       if (key == GlobalSettings::Key_View_ShowSamplingPoints)
-               on_settingViewShowSamplingPoints_changed(value);
-
-       if (key == GlobalSettings::Key_View_ShowAnalogMinorGrid)
-               on_settingViewShowAnalogMinorGrid_changed(value);
-}
-
 void MainWindow::setup_ui()
 {
        setObjectName(QString::fromUtf8("MainWindow"));
@@ -424,6 +491,7 @@ void MainWindow::setup_ui()
        icon.addFile(QString(":/icons/pulseview.png"));
        setWindowIcon(icon);
 
+       // Set up keyboard shortcuts that affect all views at once
        view_sticky_scrolling_shortcut_ = new QShortcut(QKeySequence(Qt::Key_S), this, SLOT(on_view_sticky_scrolling_shortcut()));
        view_sticky_scrolling_shortcut_->setAutoRepeat(false);
 
@@ -485,8 +553,6 @@ void MainWindow::setup_ui()
                this, SLOT(on_new_session_clicked()));
        connect(run_stop_button_, SIGNAL(clicked(bool)),
                this, SLOT(on_run_stop_clicked()));
-       connect(&session_state_mapper_, SIGNAL(mapped(QObject*)),
-               this, SLOT(on_capture_state_changed(QObject*)));
        connect(settings_button_, SIGNAL(clicked(bool)),
                this, SLOT(on_settings_clicked()));
 
@@ -501,6 +567,25 @@ void MainWindow::setup_ui()
                this, SLOT(on_focus_changed()));
 }
 
+void MainWindow::update_acq_button(Session *session)
+{
+       int state;
+       QString run_caption;
+
+       if (session) {
+               state = session->get_capture_state();
+               run_caption = session->using_file_device() ? tr("Reload") : tr("Run");
+       } else {
+               state = Session::Stopped;
+               run_caption = tr("Run");
+       }
+
+       const QIcon *icons[] = {&icon_grey_, &icon_red_, &icon_green_};
+       run_stop_button_->setIcon(*icons[state]);
+       run_stop_button_->setText((state == pv::Session::Stopped) ?
+               run_caption : tr("Stop"));
+}
+
 void MainWindow::save_ui_settings()
 {
        QSettings settings;
@@ -571,13 +656,12 @@ bool MainWindow::restoreState(const QByteArray &state, int version)
        return false;
 }
 
-void MainWindow::on_add_view(const QString &title, views::ViewType type,
-       Session *session)
+void MainWindow::on_add_view(views::ViewType type, Session *session)
 {
        // We get a pointer and need a reference
        for (shared_ptr<Session>& s : sessions_)
                if (s.get() == session)
-                       add_view(title, type, *s);
+                       add_view(type, *s);
 }
 
 void MainWindow::on_focus_changed()
@@ -614,7 +698,7 @@ void MainWindow::on_focused_session_changed(shared_ptr<Session> session)
        setWindowTitle(session->name() + " - " + WindowTitle);
 
        // Update the state of the run/stop button, too
-       on_capture_state_changed(session.get());
+       update_acq_button(session.get());
 }
 
 void MainWindow::on_new_session_clicked()
@@ -675,29 +759,40 @@ void MainWindow::on_session_name_changed()
                setWindowTitle(session->name() + " - " + WindowTitle);
 }
 
-void MainWindow::on_capture_state_changed(QObject *obj)
+void MainWindow::on_session_device_changed()
 {
-       Session *caller = qobject_cast<Session*>(obj);
+       Session *session = qobject_cast<Session*>(QObject::sender());
+       assert(session);
 
        // Ignore if caller is not the currently focused session
        // unless there is only one session
-       if ((sessions_.size() > 1) && (caller != last_focused_session_.get()))
+       if ((sessions_.size() > 1) && (session != last_focused_session_.get()))
                return;
 
-       int state = caller->get_capture_state();
+       update_acq_button(session);
+}
 
-       const QIcon *icons[] = {&icon_grey_, &icon_red_, &icon_green_};
-       run_stop_button_->setIcon(*icons[state]);
-       run_stop_button_->setText((state == pv::Session::Stopped) ?
-               tr("Run") : tr("Stop"));
+void MainWindow::on_session_capture_state_changed(int state)
+{
+       (void)state;
+
+       Session *session = qobject_cast<Session*>(QObject::sender());
+       assert(session);
+
+       // Ignore if caller is not the currently focused session
+       // unless there is only one session
+       if ((sessions_.size() > 1) && (session != last_focused_session_.get()))
+               return;
+
+       update_acq_button(session);
 }
 
-void MainWindow::on_new_view(Session *session)
+void MainWindow::on_new_view(Session *session, int view_type)
 {
        // We get a pointer and need a reference
        for (shared_ptr<Session>& s : sessions_)
                if (s.get() == session)
-                       add_view(session->name(), views::ViewTypeTrace, *s);
+                       add_view((views::ViewType)view_type, *s);
 }
 
 void MainWindow::on_view_close_clicked()
@@ -758,6 +853,53 @@ void MainWindow::on_tab_close_requested(int index)
                tr("This session contains unsaved data. Close it anyway?"),
                QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes))
                remove_session(session);
+
+       if (sessions_.empty())
+               update_acq_button(nullptr);
+}
+
+void MainWindow::on_show_decoder_selector(Session *session)
+{
+#ifdef ENABLE_DECODE
+       // Close dock widget if it's already showing and return
+       for (auto& entry : sub_windows_) {
+               QDockWidget* dock = entry.first;
+               shared_ptr<subwindows::SubWindowBase> decoder_selector =
+                       dynamic_pointer_cast<subwindows::decoder_selector::SubWindow>(entry.second);
+
+               if (decoder_selector && (&decoder_selector->session() == session)) {
+                       sub_windows_.erase(dock);
+                       dock->close();
+                       return;
+               }
+       }
+
+       // We get a pointer and need a reference
+       for (shared_ptr<Session>& s : sessions_)
+               if (s.get() == session)
+                       add_subwindow(subwindows::SubWindowTypeDecoderSelector, *s);
+#endif
+}
+
+void MainWindow::on_sub_window_close_clicked()
+{
+       // Find the dock widget that contains the close button that was clicked
+       QObject *w = QObject::sender();
+       QDockWidget *dock = nullptr;
+
+       while (w) {
+           dock = qobject_cast<QDockWidget*>(w);
+           if (dock)
+               break;
+           w = w->parent();
+       }
+
+       sub_windows_.erase(dock);
+       dock->close();
+
+       // Restore focus to the last used main view
+       if (last_focused_session_)
+               last_focused_session_->main_view()->setFocus();
 }
 
 void MainWindow::on_view_colored_bg_shortcut()
@@ -792,51 +934,6 @@ void MainWindow::on_view_show_analog_minor_grid_shortcut()
        settings.setValue(GlobalSettings::Key_View_ShowAnalogMinorGrid, !state);
 }
 
-void MainWindow::on_settingViewColoredBg_changed(const QVariant new_value)
-{
-       bool state = new_value.toBool();
-
-       for (auto& entry : view_docks_) {
-               shared_ptr<views::ViewBase> viewbase = entry.second;
-
-               // Only trace views have this setting
-               views::trace::View* view =
-                               qobject_cast<views::trace::View*>(viewbase.get());
-               if (view)
-                       view->enable_colored_bg(state);
-       }
-}
-
-void MainWindow::on_settingViewShowSamplingPoints_changed(const QVariant new_value)
-{
-       bool state = new_value.toBool();
-
-       for (auto& entry : view_docks_) {
-               shared_ptr<views::ViewBase> viewbase = entry.second;
-
-               // Only trace views have this setting
-               views::trace::View* view =
-                               qobject_cast<views::trace::View*>(viewbase.get());
-               if (view)
-                       view->enable_show_sampling_points(state);
-       }
-}
-
-void MainWindow::on_settingViewShowAnalogMinorGrid_changed(const QVariant new_value)
-{
-       bool state = new_value.toBool();
-
-       for (auto& entry : view_docks_) {
-               shared_ptr<views::ViewBase> viewbase = entry.second;
-
-               // Only trace views have this setting
-               views::trace::View* view =
-                               qobject_cast<views::trace::View*>(viewbase.get());
-               if (view)
-                       view->enable_show_analog_minor_grid(state);
-       }
-}
-
 void MainWindow::on_close_current_tab()
 {
        int tab = session_selector_.currentIndex();
index 6d92b270dc6394f4fd5c2a66bb604766044b8c98..522ab1c0478ea7d1def85a90ada942733735f182 100644 (file)
 
 #include <QMainWindow>
 #include <QShortcut>
-#include <QSignalMapper>
 #include <QTabWidget>
 #include <QToolButton>
 
-#include "globalsettings.hpp"
 #include "session.hpp"
+#include "subwindows/subwindowbase.hpp"
 #include "views/viewbase.hpp"
 
 using std::list;
@@ -62,7 +61,7 @@ class DecoderMenu;
 #endif
 }
 
-class MainWindow : public QMainWindow, public GlobalSettingsInterface
+class MainWindow : public QMainWindow
 {
        Q_OBJECT
 
@@ -79,26 +78,28 @@ public:
 
        shared_ptr<views::ViewBase> get_active_view() const;
 
-       shared_ptr<views::ViewBase> add_view(const QString &title,
-               views::ViewType type, Session &session);
+       shared_ptr<views::ViewBase> add_view(views::ViewType type, Session &session);
 
        void remove_view(shared_ptr<views::ViewBase> view);
 
+       shared_ptr<subwindows::SubWindowBase> add_subwindow(
+               subwindows::SubWindowType type, Session &session);
+
        shared_ptr<Session> add_session();
 
        void remove_session(shared_ptr<Session> session);
 
-       void add_session_with_file(string open_file_name, string open_file_format);
+       void add_session_with_file(string open_file_name, string open_file_format,
+               string open_setup_file_name);
 
        void add_default_session();
 
        void save_sessions();
        void restore_sessions();
 
-       void on_setting_changed(const QString &key, const QVariant &value);
-
 private:
        void setup_ui();
+       void update_acq_button(Session *session);
 
        void save_ui_settings();
        void restore_ui_settings();
@@ -112,8 +113,7 @@ private:
        virtual bool restoreState(const QByteArray &state, int version = 0);
 
 private Q_SLOTS:
-       void on_add_view(const QString &title, views::ViewType type,
-               Session *session);
+       void on_add_view(views::ViewType type, Session *session);
 
        void on_focus_changed();
        void on_focused_session_changed(shared_ptr<Session> session);
@@ -123,23 +123,23 @@ private Q_SLOTS:
        void on_settings_clicked();
 
        void on_session_name_changed();
-       void on_capture_state_changed(QObject *obj);
+       void on_session_device_changed();
+       void on_session_capture_state_changed(int state);
 
-       void on_new_view(Session *session);
+       void on_new_view(Session *session, int view_type);
        void on_view_close_clicked();
 
        void on_tab_changed(int index);
        void on_tab_close_requested(int index);
 
+       void on_show_decoder_selector(Session *session);
+       void on_sub_window_close_clicked();
+
        void on_view_colored_bg_shortcut();
        void on_view_sticky_scrolling_shortcut();
        void on_view_show_sampling_points_shortcut();
        void on_view_show_analog_minor_grid_shortcut();
 
-       void on_settingViewColoredBg_changed(const QVariant new_value);
-       void on_settingViewShowSamplingPoints_changed(const QVariant new_value);
-       void on_settingViewShowAnalogMinorGrid_changed(const QVariant new_value);
-
        void on_close_current_tab();
 
 private:
@@ -149,13 +149,13 @@ private:
        shared_ptr<Session> last_focused_session_;
 
        map< QDockWidget*, shared_ptr<views::ViewBase> > view_docks_;
+       map< QDockWidget*, shared_ptr<subwindows::SubWindowBase> > sub_windows_;
 
        map< shared_ptr<Session>, QMainWindow*> session_windows_;
 
        QWidget *static_tab_widget_;
        QToolButton *new_session_button_, *run_stop_button_, *settings_button_;
        QTabWidget session_selector_;
-       QSignalMapper session_state_mapper_;
 
        QIcon icon_red_;
        QIcon icon_green_;
index acd7079b7616d0a37a31d682eb7459c3bf0e5738..842a19d29305ba3db38de9a43f096ba22da45490 100644 (file)
@@ -42,6 +42,7 @@ using std::out_of_range;
 using std::shared_ptr;
 using std::unordered_set;
 using std::vector;
+using std::weak_ptr;
 
 using pv::data::SignalBase;
 using pv::data::Logic;
@@ -227,49 +228,98 @@ void Channels::populate_group(shared_ptr<ChannelGroup> group,
        if (group)
                binding = make_shared<Device>(group);
 
+       QHBoxLayout* group_layout = new QHBoxLayout();
+       layout_.addRow(group_layout);
+
        // Create a title if the group is going to have any content
        if ((!sigs.empty() || (binding && !binding->properties().empty())) && group)
        {
                QLabel *label = new QLabel(
                        QString("<h3>%1</h3>").arg(group->name().c_str()));
-               layout_.addRow(label);
+               group_layout->addWidget(label);
                group_label_map_[group] = label;
        }
 
        // Create the channel group grid
-       QGridLayout *const channel_grid = create_channel_group_grid(sigs);
-       layout_.addRow(channel_grid);
-
-       // Create the channel group options
-       if (binding) {
-               binding->add_properties_to_form(&layout_, true);
-               group_bindings_.push_back(binding);
-       }
-}
-
-QGridLayout* Channels::create_channel_group_grid(
-       const vector< shared_ptr<SignalBase> > sigs)
-{
        int row = 0, col = 0;
        QGridLayout *const grid = new QGridLayout();
+       vector<QCheckBox*> group_checkboxes, this_row;
 
        for (const shared_ptr<SignalBase>& sig : sigs) {
                assert(sig);
 
                QCheckBox *const checkbox = new QCheckBox(sig->display_name());
                check_box_mapper_.setMapping(checkbox, checkbox);
-               connect(checkbox, SIGNAL(toggled(bool)),
-                       &check_box_mapper_, SLOT(map()));
+               connect(checkbox, SIGNAL(toggled(bool)), &check_box_mapper_, SLOT(map()));
 
                grid->addWidget(checkbox, row, col);
 
+               group_checkboxes.push_back(checkbox);
+               this_row.push_back(checkbox);
+
                check_box_signal_map_[checkbox] = sig;
 
-               if (++col >= 8)
+               weak_ptr<SignalBase> weak_sig(sig);
+               connect(checkbox, &QCheckBox::toggled,
+                               [weak_sig](bool state) {
+                       auto sig = weak_sig.lock();
+                       assert(sig);
+                       sig->set_enabled(state);
+                       });
+
+               if ((++col >= 8 || &sig == &sigs.back())) {
+                       // Show buttons if there's more than one row
+                       if (sigs.size() > 8) {
+                               QPushButton *row_enable_button = new QPushButton(tr("All"), this);
+                               grid->addWidget(row_enable_button, row, 8);
+                               connect(row_enable_button, &QPushButton::clicked, row_enable_button,
+                                       [this_row]() {
+                                               for (QCheckBox *box : this_row)
+                                                       box->setChecked(true);
+                                       });
+
+                               QPushButton *row_disable_button = new QPushButton(tr("None"), this);
+                               connect(row_disable_button, &QPushButton::clicked, row_disable_button,
+                                               [this_row]() {
+                                       for (QCheckBox *box : this_row)
+                                               box->setChecked(false);
+                                       });
+                               grid->addWidget(row_disable_button, row, 9);
+                       }
+
+                       this_row.clear();
                        col = 0, row++;
+               }
+       }
+       layout_.addRow(grid);
+
+       if (sigs.size() > 1) {
+               // Create enable all/none buttons
+               QPushButton *btn_enable_all, *btn_disable_all;
+
+               btn_enable_all = new QPushButton(tr("All"));
+               btn_disable_all = new QPushButton(tr("None"));
+               group_layout->addWidget(btn_enable_all);
+               group_layout->addWidget(btn_disable_all);
+
+               connect(btn_enable_all, &QPushButton::clicked, btn_enable_all,
+                       [group_checkboxes](){
+                               for (QCheckBox *box: group_checkboxes)
+                                       box->setChecked(true);
+                       });
+
+               connect(btn_disable_all, &QPushButton::clicked, btn_disable_all,
+                       [group_checkboxes](){
+                               for (QCheckBox *box: group_checkboxes)
+                                       box->setChecked(false);
+                       });
        }
 
-       return grid;
+       // Create the channel group options
+       if (binding) {
+               binding->add_properties_to_form(&layout_, true);
+               group_bindings_.push_back(binding);
+       }
 }
 
 void Channels::showEvent(QShowEvent *event)
index 54d24340d7d85229db92576df8efc17bab24a7fc..972549a8d1c9fd2f65b63e997eb37b1d6440459f 100644 (file)
@@ -79,9 +79,6 @@ private:
        void populate_group(shared_ptr<sigrok::ChannelGroup> group,
                const vector< shared_ptr<pv::data::SignalBase> > sigs);
 
-       QGridLayout* create_channel_group_grid(
-               const vector< shared_ptr<pv::data::SignalBase> > sigs);
-
        void showEvent(QShowEvent *event);
 
 private Q_SLOTS:
index c0bcd670de26d0af866bf9cb87705ad506d5a57e..6b879cf91dcdd4e4b6127cb99a780366042d09a1 100644 (file)
@@ -17,9 +17,6 @@
  * along with this program; if not, see <http://www.gnu.org/licenses/>.
  */
 
-#include <QDebug>
-#include <QFileInfo>
-
 #include <cassert>
 #include <memory>
 #include <mutex>
 
 #include <sys/stat.h>
 
+#include <QDebug>
+#include <QFileInfo>
+
 #include "devicemanager.hpp"
 #include "mainwindow.hpp"
 #include "session.hpp"
+#include "util.hpp"
 
 #include "data/analog.hpp"
 #include "data/analogsegment.hpp"
 
 #include <libsigrokcxx/libsigrokcxx.hpp>
 
+#ifdef ENABLE_FLOW
+#include <gstreamermm.h>
+#include <libsigrokflow/libsigrokflow.hpp>
+#endif
+
 #ifdef ENABLE_DECODE
 #include <libsigrokdecode/libsigrokdecode.h>
 #include "data/decodesignal.hpp"
@@ -61,8 +67,8 @@ using std::bad_alloc;
 using std::dynamic_pointer_cast;
 using std::find_if;
 using std::function;
-using std::lock_guard;
 using std::list;
+using std::lock_guard;
 using std::make_pair;
 using std::make_shared;
 using std::map;
@@ -74,8 +80,10 @@ using std::recursive_mutex;
 using std::runtime_error;
 using std::shared_ptr;
 using std::string;
+#ifdef ENABLE_FLOW
+using std::unique_lock;
+#endif
 using std::unique_ptr;
-using std::unordered_set;
 using std::vector;
 
 using sigrok::Analog;
@@ -91,6 +99,17 @@ using sigrok::Session;
 
 using Glib::VariantBase;
 
+#ifdef ENABLE_FLOW
+using Gst::Bus;
+using Gst::ElementFactory;
+using Gst::Pipeline;
+#endif
+
+using pv::util::Timestamp;
+using pv::views::trace::Signal;
+using pv::views::trace::AnalogSignal;
+using pv::views::trace::LogicSignal;
+
 namespace pv {
 
 shared_ptr<sigrok::Context> Session::sr_context;
@@ -148,7 +167,7 @@ void Session::set_name(QString name)
        name_changed();
 }
 
-const list< shared_ptr<views::ViewBase> > Session::views() const
+const vector< shared_ptr<views::ViewBase> > Session::views() const
 {
        return views_;
 }
@@ -173,11 +192,89 @@ bool Session::data_saved() const
        return data_saved_;
 }
 
+void Session::save_setup(QSettings &settings) const
+{
+       int i = 0;
+
+       // Save channels and decoders
+       for (const shared_ptr<data::SignalBase>& base : signalbases_) {
+#ifdef ENABLE_DECODE
+               if (base->is_decode_signal()) {
+                       settings.beginGroup("decode_signal" + QString::number(i++));
+                       base->save_settings(settings);
+                       settings.endGroup();
+               } else
+#endif
+               {
+                       settings.beginGroup(base->internal_name());
+                       base->save_settings(settings);
+                       settings.endGroup();
+               }
+       }
+
+       settings.setValue("decode_signals", i);
+
+       // Save view states and their signal settings
+       // Note: main_view must be saved as view0
+       i = 0;
+       settings.beginGroup("view" + QString::number(i++));
+       main_view_->save_settings(settings);
+       settings.endGroup();
+
+       for (const shared_ptr<views::ViewBase>& view : views_) {
+               if (view != main_view_) {
+                       settings.beginGroup("view" + QString::number(i++));
+                       settings.setValue("type", view->get_type());
+                       view->save_settings(settings);
+                       settings.endGroup();
+               }
+       }
+
+       settings.setValue("views", i);
+
+       int view_id = 0;
+       i = 0;
+       for (const shared_ptr<views::ViewBase>& vb : views_) {
+               shared_ptr<views::trace::View> tv = dynamic_pointer_cast<views::trace::View>(vb);
+               if (tv) {
+                       for (const shared_ptr<views::trace::TimeItem>& time_item : tv->time_items()) {
+
+                               const shared_ptr<views::trace::Flag> flag =
+                                       dynamic_pointer_cast<views::trace::Flag>(time_item);
+                               if (flag) {
+                                       if (!flag->enabled())
+                                               continue;
+
+                                       settings.beginGroup("meta_obj" + QString::number(i++));
+                                       settings.setValue("type", "time_marker");
+                                       settings.setValue("assoc_view", view_id);
+                                       GlobalSettings::store_timestamp(settings, "time", flag->time());
+                                       settings.setValue("text", flag->get_text());
+                                       settings.endGroup();
+                               }
+                       }
+
+                       if (tv->cursors_shown()) {
+                               settings.beginGroup("meta_obj" + QString::number(i++));
+                               settings.setValue("type", "selection");
+                               settings.setValue("assoc_view", view_id);
+                               const shared_ptr<views::trace::CursorPair> cp = tv->cursors();
+                               GlobalSettings::store_timestamp(settings, "start_time", cp->first()->time());
+                               GlobalSettings::store_timestamp(settings, "end_time", cp->second()->time());
+                               settings.endGroup();
+                       }
+               }
+
+               view_id++;
+       }
+
+       settings.setValue("meta_objs", i);
+}
+
 void Session::save_settings(QSettings &settings) const
 {
        map<string, string> dev_info;
        list<string> key_list;
-       int decode_signals = 0, views = 0;
 
        if (device_) {
                shared_ptr<devices::HardwareDevice> hw_device =
@@ -227,39 +324,77 @@ void Session::save_settings(QSettings &settings) const
                        settings.endGroup();
                }
 
-               // Save channels and decoders
-               for (const shared_ptr<data::SignalBase>& base : signalbases_) {
+               save_setup(settings);
+       }
+}
+
+void Session::restore_setup(QSettings &settings)
+{
+       // Restore channels
+       for (shared_ptr<data::SignalBase> base : signalbases_) {
+               settings.beginGroup(base->internal_name());
+               base->restore_settings(settings);
+               settings.endGroup();
+       }
+
+       // Restore decoders
 #ifdef ENABLE_DECODE
-                       if (base->is_decode_signal()) {
-                               settings.beginGroup("decode_signal" + QString::number(decode_signals++));
-                               base->save_settings(settings);
-                               settings.endGroup();
-                       } else
+       int decode_signals = settings.value("decode_signals").toInt();
+
+       for (int i = 0; i < decode_signals; i++) {
+               settings.beginGroup("decode_signal" + QString::number(i));
+               shared_ptr<data::DecodeSignal> signal = add_decode_signal();
+               signal->restore_settings(settings);
+               settings.endGroup();
+       }
 #endif
-                       {
-                               settings.beginGroup(base->internal_name());
-                               base->save_settings(settings);
-                               settings.endGroup();
-                       }
-               }
 
-               settings.setValue("decode_signals", decode_signals);
+       // Restore views
+       int views = settings.value("views").toInt();
+
+       for (int i = 0; i < views; i++) {
+               settings.beginGroup("view" + QString::number(i));
+
+               if (i > 0) {
+                       views::ViewType type = (views::ViewType)settings.value("type").toInt();
+                       add_view(type, this);
+                       views_.back()->restore_settings(settings);
+               } else
+                       main_view_->restore_settings(settings);
 
-               // Save view states and their signal settings
-               // Note: main_view must be saved as view0
-               settings.beginGroup("view" + QString::number(views++));
-               main_view_->save_settings(settings);
                settings.endGroup();
+       }
 
-               for (const shared_ptr<views::ViewBase>& view : views_) {
-                       if (view != main_view_) {
-                               settings.beginGroup("view" + QString::number(views++));
-                               view->save_settings(settings);
-                               settings.endGroup();
-                       }
+       // Restore meta objects like markers and cursors
+       int meta_objs = settings.value("meta_objs").toInt();
+
+       for (int i = 0; i < meta_objs; i++) {
+               settings.beginGroup("meta_obj" + QString::number(i));
+
+               shared_ptr<views::ViewBase> vb;
+               shared_ptr<views::trace::View> tv;
+               if (settings.contains("assoc_view"))
+                       vb = views_.at(settings.value("assoc_view").toInt());
+
+               if (vb)
+                       tv = dynamic_pointer_cast<views::trace::View>(vb);
+
+               const QString type = settings.value("type").toString();
+
+               if ((type == "time_marker") && tv) {
+                       Timestamp ts = GlobalSettings::restore_timestamp(settings, "time");
+                       shared_ptr<views::trace::Flag> flag = tv->add_flag(ts);
+                       flag->set_text(settings.value("text").toString());
                }
 
-               settings.setValue("views", views);
+               if ((type == "selection") && tv) {
+                       Timestamp start = GlobalSettings::restore_timestamp(settings, "start_time");
+                       Timestamp end = GlobalSettings::restore_timestamp(settings, "end_time");
+                       tv->set_cursors(start, end);
+                       tv->show_cursors();
+               }
+
+               settings.endGroup();
        }
 }
 
@@ -267,7 +402,7 @@ void Session::restore_settings(QSettings &settings)
 {
        shared_ptr<devices::Device> device;
 
-       QString device_type = settings.value("device_type").toString();
+       const QString device_type = settings.value("device_type").toString();
 
        if (device_type == "hardware") {
                map<string, string> dev_info;
@@ -303,7 +438,7 @@ void Session::restore_settings(QSettings &settings)
        if ((device_type == "sessionfile") || (device_type == "inputfile")) {
                if (device_type == "sessionfile") {
                        settings.beginGroup("device");
-                       QString filename = settings.value("filename").toString();
+                       const QString filename = settings.value("filename").toString();
                        settings.endGroup();
 
                        if (QFileInfo(filename).isReadable()) {
@@ -331,42 +466,8 @@ void Session::restore_settings(QSettings &settings)
                }
        }
 
-       if (device) {
-               // Restore channels
-               for (shared_ptr<data::SignalBase> base : signalbases_) {
-                       settings.beginGroup(base->internal_name());
-                       base->restore_settings(settings);
-                       settings.endGroup();
-               }
-
-               // Restore decoders
-#ifdef ENABLE_DECODE
-               int decode_signals = settings.value("decode_signals").toInt();
-
-               for (int i = 0; i < decode_signals; i++) {
-                       settings.beginGroup("decode_signal" + QString::number(i));
-                       shared_ptr<data::DecodeSignal> signal = add_decode_signal();
-                       signal->restore_settings(settings);
-                       settings.endGroup();
-               }
-#endif
-
-               // Restore views
-               int views = settings.value("views").toInt();
-
-               for (int i = 0; i < views; i++) {
-                       settings.beginGroup("view" + QString::number(i));
-
-                       if (i > 0) {
-                               views::ViewType type = (views::ViewType)settings.value("type").toInt();
-                               add_view(name_, type, this);
-                               views_.back()->restore_settings(settings);
-                       } else
-                               main_view_->restore_settings(settings);
-
-                       settings.endGroup();
-               }
-       }
+       if (device)
+               restore_setup(settings);
 }
 
 void Session::select_device(shared_ptr<devices::Device> device)
@@ -456,6 +557,17 @@ void Session::set_default_device()
        set_device((iter == devices.end()) ? devices.front() : *iter);
 }
 
+bool Session::using_file_device() const
+{
+       shared_ptr<devices::SessionFile> sessionfile_device =
+               dynamic_pointer_cast<devices::SessionFile>(device_);
+
+       shared_ptr<devices::InputFile> inputfile_device =
+               dynamic_pointer_cast<devices::InputFile>(device_);
+
+       return (sessionfile_device || inputfile_device);
+}
+
 /**
  * Convert generic options to data types that are specific to InputFormat.
  *
@@ -501,7 +613,8 @@ Session::input_format_options(vector<string> user_spec,
        return result;
 }
 
-void Session::load_init_file(const string &file_name, const string &format)
+void Session::load_init_file(const string &file_name,
+       const string &format, const string &setup_file_name)
 {
        shared_ptr<InputFormat> input_format;
        map<string, Glib::VariantBase> input_opts;
@@ -525,12 +638,12 @@ void Session::load_init_file(const string &file_name, const string &format)
                        input_format->options());
        }
 
-       load_file(QString::fromStdString(file_name), input_format, input_opts);
+       load_file(QString::fromStdString(file_name), QString::fromStdString(setup_file_name),
+               input_format, input_opts);
 }
 
-void Session::load_file(QString file_name,
-       shared_ptr<sigrok::InputFormat> format,
-       const map<string, Glib::VariantBase> &options)
+void Session::load_file(QString file_name, QString setup_file_name,
+       shared_ptr<sigrok::InputFormat> format, const map<string, Glib::VariantBase> &options)
 {
        const QString errorMessage(
                QString("Failed to load file %1").arg(file_name));
@@ -552,12 +665,24 @@ void Session::load_file(QString file_name,
                                        device_manager_.context(),
                                        file_name.toStdString())));
        } catch (Error& e) {
-               MainWindow::show_session_error(tr("Failed to load ") + file_name, e.what());
+               MainWindow::show_session_error(tr("Failed to load %1").arg(file_name), e.what());
                set_default_device();
                main_bar_->update_device_list();
                return;
        }
 
+       // Use the input file with .pvs extension if no setup file was given
+       if (setup_file_name.isEmpty()) {
+               setup_file_name = file_name;
+               setup_file_name.truncate(setup_file_name.lastIndexOf('.'));
+               setup_file_name.append(".pvs");
+       }
+
+       if (QFileInfo::exists(setup_file_name) && QFileInfo(setup_file_name).isReadable()) {
+               QSettings settings_storage(setup_file_name, QSettings::IniFormat);
+               restore_setup(settings_storage);
+       }
+
        main_bar_->update_device_list();
 
        start_capture([&, errorMessage](QString infoMessage) {
@@ -626,9 +751,8 @@ void Session::stop_capture()
 
 void Session::register_view(shared_ptr<views::ViewBase> view)
 {
-       if (views_.empty()) {
+       if (views_.empty())
                main_view_ = view;
-       }
 
        views_.push_back(view);
 
@@ -636,35 +760,29 @@ void Session::register_view(shared_ptr<views::ViewBase> view)
        update_signals();
 
        // Add all other signals
-       unordered_set< shared_ptr<data::SignalBase> > view_signalbases =
-               view->signalbases();
-
-       views::trace::View *trace_view =
-               qobject_cast<views::trace::View*>(view.get());
-
-       if (trace_view) {
-               for (const shared_ptr<data::SignalBase>& signalbase : signalbases_) {
-                       const int sb_exists = count_if(
-                               view_signalbases.cbegin(), view_signalbases.cend(),
-                               [&](const shared_ptr<data::SignalBase> &sb) {
-                                       return sb == signalbase;
-                               });
-                       // Add the signal to the view as it doesn't have it yet
-                       if (!sb_exists)
-                               switch (signalbase->type()) {
-                               case data::SignalBase::AnalogChannel:
-                               case data::SignalBase::LogicChannel:
-                               case data::SignalBase::DecodeChannel:
+       vector< shared_ptr<data::SignalBase> > view_signalbases = view->signalbases();
+
+       for (const shared_ptr<data::SignalBase>& signalbase : signalbases_) {
+               const int sb_exists = count_if(
+                       view_signalbases.cbegin(), view_signalbases.cend(),
+                       [&](const shared_ptr<data::SignalBase> &sb) {
+                               return sb == signalbase;
+                       });
+
+               // Add the signal to the view if it doesn't have it yet
+               if (!sb_exists)
+                       switch (signalbase->type()) {
+                       case data::SignalBase::AnalogChannel:
+                       case data::SignalBase::LogicChannel:
+                       case data::SignalBase::MathChannel:
+                               view->add_signalbase(signalbase);
+                               break;
+                       case data::SignalBase::DecodeChannel:
 #ifdef ENABLE_DECODE
-                                       trace_view->add_decode_signal(
-                                               dynamic_pointer_cast<data::DecodeSignal>(signalbase));
+                               view->add_decode_signal(dynamic_pointer_cast<data::DecodeSignal>(signalbase));
 #endif
-                                       break;
-                               case data::SignalBase::MathChannel:
-                                       // TBD
-                                       break;
-                               }
-               }
+                               break;
+                       }
        }
 
        signals_changed();
@@ -672,7 +790,9 @@ void Session::register_view(shared_ptr<views::ViewBase> view)
 
 void Session::deregister_view(shared_ptr<views::ViewBase> view)
 {
-       views_.remove_if([&](shared_ptr<views::ViewBase> v) { return v == view; });
+       views_.erase(std::remove_if(views_.begin(), views_.end(),
+               [&](shared_ptr<views::ViewBase> v) { return v == view; }),
+               views_.end());
 
        if (views_.empty()) {
                main_view_.reset();
@@ -732,7 +852,7 @@ vector<util::Timestamp> Session::get_triggers(uint32_t segment_id) const
        return result;
 }
 
-const unordered_set< shared_ptr<data::SignalBase> > Session::signalbases() const
+const vector< shared_ptr<data::SignalBase> > Session::signalbases() const
 {
        return signalbases_;
 }
@@ -757,7 +877,7 @@ shared_ptr<data::DecodeSignal> Session::add_decode_signal()
                // Create the decode signal
                signal = make_shared<data::DecodeSignal>(*this);
 
-               signalbases_.insert(signal);
+               signalbases_.push_back(signal);
 
                // Add the decode signal to all views
                for (shared_ptr<views::ViewBase>& view : views_)
@@ -774,7 +894,9 @@ shared_ptr<data::DecodeSignal> Session::add_decode_signal()
 
 void Session::remove_decode_signal(shared_ptr<data::DecodeSignal> signal)
 {
-       signalbases_.erase(signal);
+       signalbases_.erase(std::remove_if(signalbases_.begin(), signalbases_.end(),
+               [&](shared_ptr<data::SignalBase> s) { return s == signal; }),
+               signalbases_.end());
 
        for (shared_ptr<views::ViewBase>& view : views_)
                view->remove_decode_signal(signal);
@@ -787,6 +909,11 @@ void Session::set_capture_state(capture_state state)
 {
        bool changed;
 
+       if (state == Running)
+               acq_time_.restart();
+       if (state == Stopped)
+               qDebug("Acquisition took %.2f s", acq_time_.elapsed() / 1000.);
+
        {
                lock_guard<mutex> lock(sampling_mutex_);
                changed = capture_state_ != state;
@@ -853,18 +980,17 @@ void Session::update_signals()
                        qobject_cast<views::trace::View*>(viewbase.get());
 
                if (trace_view) {
-                       unordered_set< shared_ptr<views::trace::Signal> >
-                               prev_sigs(trace_view->signals());
+                       vector< shared_ptr<Signal> > prev_sigs(trace_view->signals());
                        trace_view->clear_signals();
 
                        for (auto channel : sr_dev->channels()) {
                                shared_ptr<data::SignalBase> signalbase;
-                               shared_ptr<views::trace::Signal> signal;
+                               shared_ptr<Signal> signal;
 
                                // Find the channel in the old signals
                                const auto iter = find_if(
                                        prev_sigs.cbegin(), prev_sigs.cend(),
-                                       [&](const shared_ptr<views::trace::Signal> &s) {
+                                       [&](const shared_ptr<Signal> &s) {
                                                return s->base()->channel() == channel;
                                        });
                                if (iter != prev_sigs.end()) {
@@ -878,12 +1004,14 @@ void Session::update_signals()
                                                if (b->channel() == channel)
                                                        signalbase = b;
 
+                                       shared_ptr<Signal> signal;
+
                                        switch(channel->type()->id()) {
                                        case SR_CHANNEL_LOGIC:
                                                if (!signalbase) {
                                                        signalbase = make_shared<data::SignalBase>(channel,
                                                                data::SignalBase::LogicChannel);
-                                                       signalbases_.insert(signalbase);
+                                                       signalbases_.push_back(signalbase);
 
                                                        all_signal_data_.insert(logic_data_);
                                                        signalbase->set_data(logic_data_);
@@ -892,10 +1020,7 @@ void Session::update_signals()
                                                                signalbase.get(), SLOT(on_capture_state_changed(int)));
                                                }
 
-                                               signal = shared_ptr<views::trace::Signal>(
-                                                       new views::trace::LogicSignal(*this,
-                                                               device_, signalbase));
-                                               trace_view->add_signal(signal);
+                                               signal = shared_ptr<Signal>(new LogicSignal(*this, device_, signalbase));
                                                break;
 
                                        case SR_CHANNEL_ANALOG:
@@ -903,7 +1028,7 @@ void Session::update_signals()
                                                if (!signalbase) {
                                                        signalbase = make_shared<data::SignalBase>(channel,
                                                                data::SignalBase::AnalogChannel);
-                                                       signalbases_.insert(signalbase);
+                                                       signalbases_.push_back(signalbase);
 
                                                        shared_ptr<data::Analog> data(new data::Analog());
                                                        all_signal_data_.insert(data);
@@ -913,10 +1038,7 @@ void Session::update_signals()
                                                                signalbase.get(), SLOT(on_capture_state_changed(int)));
                                                }
 
-                                               signal = shared_ptr<views::trace::Signal>(
-                                                       new views::trace::AnalogSignal(
-                                                               *this, signalbase));
-                                               trace_view->add_signal(signal);
+                                               signal = shared_ptr<Signal>(new AnalogSignal(*this, signalbase));
                                                break;
                                        }
 
@@ -924,6 +1046,17 @@ void Session::update_signals()
                                                assert(false);
                                                break;
                                        }
+
+                                       // New views take their signal settings from the main view
+                                       if (!viewbase->is_main_view()) {
+                                               shared_ptr<pv::views::trace::View> main_tv =
+                                                       dynamic_pointer_cast<pv::views::trace::View>(main_view_);
+                                               shared_ptr<Signal> main_signal =
+                                                       main_tv->get_signal_by_signalbase(signalbase);
+                                               signal->restore_settings(main_signal->save_settings());
+                                       }
+
+                                       trace_view->add_signal(signal);
                                }
                        }
                }
@@ -947,6 +1080,35 @@ void Session::sample_thread_proc(function<void (const QString)> error_handler)
 {
        assert(error_handler);
 
+#ifdef ENABLE_FLOW
+       pipeline_ = Pipeline::create();
+
+       source_ = ElementFactory::create_element("filesrc", "source");
+       sink_ = RefPtr<AppSink>::cast_dynamic(ElementFactory::create_element("appsink", "sink"));
+
+       pipeline_->add(source_)->add(sink_);
+       source_->link(sink_);
+
+       source_->set_property("location", Glib::ustring("/tmp/dummy_binary"));
+
+       sink_->set_property("emit-signals", TRUE);
+       sink_->signal_new_sample().connect(sigc::mem_fun(*this, &Session::on_gst_new_sample));
+
+       // Get the bus from the pipeline and add a bus watch to the default main context
+       RefPtr<Bus> bus = pipeline_->get_bus();
+       bus->add_watch(sigc::mem_fun(this, &Session::on_gst_bus_message));
+
+       // Start pipeline and Wait until it finished processing
+       pipeline_done_interrupt_ = false;
+       pipeline_->set_state(Gst::STATE_PLAYING);
+
+       unique_lock<mutex> pipeline_done_lock_(pipeline_done_mutex_);
+       pipeline_done_cond_.wait(pipeline_done_lock_);
+
+       // Let the pipeline free all resources
+       pipeline_->set_state(Gst::STATE_NULL);
+
+#else
        if (!device_)
                return;
 
@@ -982,6 +1144,10 @@ void Session::sample_thread_proc(function<void (const QString)> error_handler)
                error_handler(e.what());
                set_capture_state(Stopped);
                return;
+       } catch (QString& e) {
+               error_handler(e);
+               set_capture_state(Stopped);
+               return;
        }
 
        set_capture_state(Stopped);
@@ -989,6 +1155,7 @@ void Session::sample_thread_proc(function<void (const QString)> error_handler)
        // Confirm that SR_DF_END was received
        if (cur_logic_segment_)
                qDebug() << "WARNING: SR_DF_END was not received.";
+#endif
 
        // Optimize memory usage
        free_unused_memory();
@@ -1065,6 +1232,49 @@ void Session::signal_segment_completed()
                segment_completed(segment_id);
 }
 
+#ifdef ENABLE_FLOW
+bool Session::on_gst_bus_message(const Glib::RefPtr<Gst::Bus>& bus, const Glib::RefPtr<Gst::Message>& message)
+{
+       (void)bus;
+
+       if ((message->get_source() == pipeline_) && \
+               ((message->get_message_type() == Gst::MESSAGE_EOS)))
+               pipeline_done_cond_.notify_one();
+
+       // TODO Also evaluate MESSAGE_STREAM_STATUS to receive error notifications
+
+       return true;
+}
+
+Gst::FlowReturn Session::on_gst_new_sample()
+{
+       RefPtr<Gst::Sample> sample = sink_->pull_sample();
+       RefPtr<Gst::Buffer> buf = sample->get_buffer();
+
+       for (uint32_t block_id = 0; block_id < buf->n_memory(); block_id++) {
+               RefPtr<Gst::Memory> buf_mem = buf->get_memory(block_id);
+               Gst::MapInfo mapinfo;
+               buf_mem->map(mapinfo, Gst::MAP_READ);
+
+               shared_ptr<sigrok::Packet> logic_packet =
+                       sr_context->create_logic_packet(mapinfo.get_data(), buf->get_size(), 1);
+
+               try {
+                       feed_in_logic(dynamic_pointer_cast<sigrok::Logic>(logic_packet->payload()));
+               } catch (bad_alloc&) {
+                       out_of_memory_ = true;
+                       device_->stop();
+                       buf_mem->unmap(mapinfo);
+                       return Gst::FLOW_ERROR;
+               }
+
+               buf_mem->unmap(mapinfo);
+       }
+
+       return Gst::FLOW_OK;
+}
+#endif
+
 void Session::feed_in_header()
 {
        // Nothing to do here for now
@@ -1163,6 +1373,9 @@ void Session::feed_in_logic(shared_ptr<Logic> logic)
                return;
        }
 
+       if (logic->unit_size() > 8)
+               throw QString(tr("Can't handle more than 64 logic channels."));
+
        if (!cur_samplerate_)
                try {
                        cur_samplerate_ = device_->read_config<uint64_t>(ConfigKey::SAMPLERATE);
@@ -1348,4 +1561,19 @@ void Session::on_data_saved()
        data_saved_ = true;
 }
 
+#ifdef ENABLE_DECODE
+void Session::on_new_decoders_selected(vector<const srd_decoder*> decoders)
+{
+       assert(decoders.size() > 0);
+
+       shared_ptr<data::DecodeSignal> signal = add_decode_signal();
+
+       if (signal)
+               for (unsigned int i = 0; i < decoders.size(); i++) {
+                       const srd_decoder* d = decoders[i];
+                       signal->stack_decoder(d, !(i < decoders.size() - 1));
+               }
+}
+#endif
+
 } // namespace pv
index 2ee31cfee5afb8ef6de44171322d1d20086e755a..095c4dd0217dd51f490c3d4d99bbc3d952b773b9 100644 (file)
 #ifndef PULSEVIEW_PV_SESSION_HPP
 #define PULSEVIEW_PV_SESSION_HPP
 
+#ifdef ENABLE_FLOW
+#include <atomic>
+#include <condition_variable>
+#endif
+
 #include <functional>
 #include <map>
 #include <memory>
 #include <QObject>
 #include <QSettings>
 #include <QString>
+#include <QElapsedTimer>
+
+#ifdef ENABLE_FLOW
+#include <gstreamermm.h>
+#include <libsigrokflow/libsigrokflow.hpp>
+#endif
 
 #include "util.hpp"
 #include "views/viewbase.hpp"
 
+
 using std::function;
-using std::list;
 using std::map;
 using std::mutex;
 using std::recursive_mutex;
@@ -46,6 +57,13 @@ using std::shared_ptr;
 using std::string;
 using std::unordered_set;
 
+#ifdef ENABLE_FLOW
+using Glib::RefPtr;
+using Gst::AppSink;
+using Gst::Element;
+using Gst::Pipeline;
+#endif
+
 struct srd_decoder;
 struct srd_channel;
 
@@ -120,7 +138,7 @@ public:
 
        void set_name(QString name);
 
-       const list< shared_ptr<views::ViewBase> > views() const;
+       const vector< shared_ptr<views::ViewBase> > views() const;
 
        shared_ptr<views::ViewBase> main_view() const;
 
@@ -133,8 +151,12 @@ public:
         */
        bool data_saved() const;
 
+       void save_setup(QSettings &settings) const;
+
        void save_settings(QSettings &settings) const;
 
+       void restore_setup(QSettings &settings);
+
        void restore_settings(QSettings &settings);
 
        /**
@@ -149,9 +171,12 @@ public:
 
        void set_default_device();
 
-       void load_init_file(const string &file_name, const string &format);
+       bool using_file_device() const;
+
+       void load_init_file(const string &file_name, const string &format,
+               const string &setup_file_name);
 
-       void load_file(QString file_name,
+       void load_file(QString file_name, QString setup_file_name = QString(),
                shared_ptr<sigrok::InputFormat> format = nullptr,
                const map<string, Glib::VariantBase> &options =
                        map<string, Glib::VariantBase>());
@@ -174,7 +199,7 @@ public:
 
        bool has_view(shared_ptr<views::ViewBase> view);
 
-       const unordered_set< shared_ptr<data::SignalBase> > signalbases() const;
+       const vector< shared_ptr<data::SignalBase> > signalbases() const;
 
        bool all_segments_complete(uint32_t segment_id) const;
 
@@ -203,6 +228,12 @@ private:
        void signal_new_segment();
        void signal_segment_completed();
 
+#ifdef ENABLE_FLOW
+       bool on_gst_bus_message(const Glib::RefPtr<Gst::Bus>& bus, const Glib::RefPtr<Gst::Message>& message);
+
+       Gst::FlowReturn on_gst_new_sample();
+#endif
+
        void feed_in_header();
 
        void feed_in_meta(shared_ptr<sigrok::Meta> meta);
@@ -234,18 +265,21 @@ Q_SIGNALS:
 
        void data_received();
 
-       void add_view(const QString &title, views::ViewType type,
-               Session *session);
+       void add_view(views::ViewType type, Session *session);
 
 public Q_SLOTS:
        void on_data_saved();
 
+#ifdef ENABLE_DECODE
+       void on_new_decoders_selected(vector<const srd_decoder*> decoders);
+#endif
+
 private:
        DeviceManager &device_manager_;
        shared_ptr<devices::Device> device_;
        QString default_name_, name_;
 
-       list< shared_ptr<views::ViewBase> > views_;
+       vector< shared_ptr<views::ViewBase> > views_;
        shared_ptr<pv::views::ViewBase> main_view_;
 
        shared_ptr<pv::toolbars::MainBar> main_bar_;
@@ -253,7 +287,7 @@ private:
        mutable mutex sampling_mutex_; //!< Protects access to capture_state_.
        capture_state capture_state_;
 
-       unordered_set< shared_ptr<data::SignalBase> > signalbases_;
+       vector< shared_ptr<data::SignalBase> > signalbases_;
        unordered_set< shared_ptr<data::SignalData> > all_signal_data_;
 
        /// trigger_list_ contains pairs of <segment_id, timestamp> values.
@@ -272,6 +306,18 @@ private:
        bool out_of_memory_;
        bool data_saved_;
        bool frame_began_;
+
+       QElapsedTimer acq_time_;
+
+#ifdef ENABLE_FLOW
+       RefPtr<Pipeline> pipeline_;
+       RefPtr<Element> source_;
+       RefPtr<AppSink> sink_;
+
+       mutable mutex pipeline_done_mutex_;
+       mutable condition_variable pipeline_done_cond_;
+       atomic<bool> pipeline_done_interrupt_;
+#endif
 };
 
 } // namespace pv
index ee1a3a03f096cf5c55d3a0ce16ea3f3c85496669..ed0b0defaaac41034bdf843e5858cae89f40fe19 100644 (file)
@@ -21,6 +21,8 @@
 
 #include "storesession.hpp"
 
+#include <QSettings>
+
 #include <pv/data/analog.hpp>
 #include <pv/data/analogsegment.hpp>
 #include <pv/data/logic.hpp>
@@ -28,6 +30,7 @@
 #include <pv/data/signalbase.hpp>
 #include <pv/devicemanager.hpp>
 #include <pv/devices/device.hpp>
+#include <pv/globalsettings.hpp>
 #include <pv/session.hpp>
 
 #include <libsigrokcxx/libsigrokcxx.hpp>
@@ -42,7 +45,6 @@ using std::mutex;
 using std::pair;
 using std::shared_ptr;
 using std::string;
-using std::unordered_set;
 using std::vector;
 
 using Glib::VariantBase;
@@ -90,7 +92,7 @@ const QString& StoreSession::error() const
 
 bool StoreSession::start()
 {
-       const unordered_set< shared_ptr<data::SignalBase> > sigs(session_.signalbases());
+       const vector< shared_ptr<data::SignalBase> > sigs(session_.signalbases());
 
        shared_ptr<data::Segment> any_segment;
        shared_ptr<data::LogicSegment> lsegment;
@@ -189,6 +191,20 @@ bool StoreSession::start()
 
        thread_ = std::thread(&StoreSession::store_proc, this,
                achannel_list, asegment_list, lsegment);
+
+       // Save session setup if we're saving to srzip and the user wants it
+       GlobalSettings settings;
+       bool save_with_setup = settings.value(GlobalSettings::Key_General_SaveWithSetup).toBool();
+
+       if ((output_format_->name() == "srzip") && (save_with_setup)) {
+               QString setup_file_name = QString::fromStdString(file_name_);
+               setup_file_name.truncate(setup_file_name.lastIndexOf('.'));
+               setup_file_name.append(".pvs");
+
+               QSettings settings_storage(setup_file_name, QSettings::IniFormat);
+               session_.save_setup(settings_storage);
+       }
+
        return true;
 }
 
@@ -234,6 +250,7 @@ void StoreSession::store_proc(vector< shared_ptr<data::SignalBase> > achannel_li
        const unsigned int samples_per_block =
                min(asamples_per_block, lsamples_per_block);
 
+       const auto context = session_.device_manager().context();
        while (!interrupt_ && sample_count_) {
                progress_updated();
 
@@ -241,8 +258,6 @@ void StoreSession::store_proc(vector< shared_ptr<data::SignalBase> > achannel_li
                        min((uint64_t)samples_per_block, sample_count_);
 
                try {
-                       const auto context = session_.device_manager().context();
-
                        for (unsigned int i = 0; i < achannel_list.size(); i++) {
                                shared_ptr<sigrok::Channel> achannel = (achannel_list.at(i))->channel();
                                shared_ptr<data::AnalogSegment> asegment = asegment_list.at(i);
@@ -286,6 +301,11 @@ void StoreSession::store_proc(vector< shared_ptr<data::SignalBase> > achannel_li
                units_stored_ = unit_count_ - (sample_count_ >> progress_scale);
        }
 
+       auto dfend = context->create_end_packet();
+       const string ldata_str = output_->receive(dfend);
+       if (output_stream_.is_open())
+               output_stream_ << ldata_str;
+
        // Zeroing the progress variables indicates completion
        units_stored_ = unit_count_ = 0;
 
diff --git a/pv/subwindows/decoder_selector/item.cpp b/pv/subwindows/decoder_selector/item.cpp
new file mode 100644 (file)
index 0000000..0ce5ab7
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2018 Soeren Apel <soeren@apelpie.net>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "subwindow.hpp"
+
+using std::out_of_range;
+
+namespace pv {
+namespace subwindows {
+namespace decoder_selector {
+
+DecoderCollectionItem::DecoderCollectionItem(const vector<QVariant>& data,
+       shared_ptr<DecoderCollectionItem> parent) :
+       data_(data),
+       parent_(parent)
+{
+}
+
+void DecoderCollectionItem::appendSubItem(shared_ptr<DecoderCollectionItem> item)
+{
+       subItems_.push_back(item);
+}
+
+shared_ptr<DecoderCollectionItem> DecoderCollectionItem::subItem(int row) const
+{
+       try {
+               return subItems_.at(row);
+       } catch (out_of_range&) {
+               return nullptr;
+       }
+}
+
+shared_ptr<DecoderCollectionItem> DecoderCollectionItem::parent() const
+{
+       return parent_;
+}
+
+shared_ptr<DecoderCollectionItem> DecoderCollectionItem::findSubItem(
+       const QVariant& value, int column)
+{
+       for (shared_ptr<DecoderCollectionItem> item : subItems_)
+               if (item->data(column) == value)
+                       return item;
+
+       return nullptr;
+}
+
+int DecoderCollectionItem::subItemCount() const
+{
+       return subItems_.size();
+}
+
+int DecoderCollectionItem::columnCount() const
+{
+       return data_.size();
+}
+
+int DecoderCollectionItem::row() const
+{
+       if (parent_)
+               for (size_t i = 0; i < parent_->subItems_.size(); i++)
+                       if (parent_->subItems_.at(i).get() == const_cast<DecoderCollectionItem*>(this))
+                               return i;
+
+       return 0;
+}
+
+QVariant DecoderCollectionItem::data(int column) const
+{
+       try {
+               return data_.at(column);
+       } catch (out_of_range&) {
+               return QVariant();
+       }
+}
+
+} // namespace decoder_selector
+} // namespace subwindows
+} // namespace pv
diff --git a/pv/subwindows/decoder_selector/model.cpp b/pv/subwindows/decoder_selector/model.cpp
new file mode 100644 (file)
index 0000000..ebdfd9d
--- /dev/null
@@ -0,0 +1,206 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2018 Soeren Apel <soeren@apelpie.net>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QString>
+
+#include "subwindow.hpp"
+
+#include <libsigrokdecode/libsigrokdecode.h>
+
+#define DECODERS_HAVE_TAGS \
+       ((SRD_PACKAGE_VERSION_MAJOR > 0) || \
+        (SRD_PACKAGE_VERSION_MAJOR == 0) && (SRD_PACKAGE_VERSION_MINOR > 5))
+
+using std::make_shared;
+
+namespace pv {
+namespace subwindows {
+namespace decoder_selector {
+
+DecoderCollectionModel::DecoderCollectionModel(QObject* parent) :
+       QAbstractItemModel(parent)
+{
+       vector<QVariant> header_data;
+       header_data.emplace_back(tr("Decoder"));     // Column #0
+       header_data.emplace_back(tr("Name"));        // Column #1
+       header_data.emplace_back(tr("ID"));          // Column #2
+       root_ = make_shared<DecoderCollectionItem>(header_data);
+
+       // Note: the tag groups are sub-items of the root item
+
+       // Create "all decoders" group
+       vector<QVariant> item_data;
+       item_data.emplace_back(tr("All Decoders"));
+       // Add dummy entries to make the row count the same as the
+       // sub-item size, or else we can't query sub-item data
+       item_data.emplace_back();
+       item_data.emplace_back();
+       shared_ptr<DecoderCollectionItem> group_item_all =
+               make_shared<DecoderCollectionItem>(item_data, root_);
+       root_->appendSubItem(group_item_all);
+
+       for (GSList* li = (GSList*)srd_decoder_list(); li; li = li->next) {
+               const srd_decoder *const d = (srd_decoder*)li->data;
+               assert(d);
+
+               const QString id = QString::fromUtf8(d->id);
+               const QString name = QString::fromUtf8(d->name);
+               const QString long_name = QString::fromUtf8(d->longname);
+
+               // Add decoder to the "all decoders" group
+               item_data.clear();
+               item_data.emplace_back(name);
+               item_data.emplace_back(long_name);
+               item_data.emplace_back(id);
+               shared_ptr<DecoderCollectionItem> decoder_item_all =
+                       make_shared<DecoderCollectionItem>(item_data, group_item_all);
+               group_item_all->appendSubItem(decoder_item_all);
+
+               // Add decoder to all relevant groups using the tag information
+#if DECODERS_HAVE_TAGS
+               for (GSList* ti = (GSList*)d->tags; ti; ti = ti->next) {
+                       const QString tag = tr((char*)ti->data);
+                       const QVariant tag_var = QVariant(tag);
+
+                       // Find tag group and create it if it doesn't exist yet
+                       shared_ptr<DecoderCollectionItem> group_item =
+                               root_->findSubItem(tag_var, 0);
+
+                       if (!group_item) {
+                               item_data.clear();
+                               item_data.emplace_back(tag);
+                               // Add dummy entries to make the row count the same as the
+                               // sub-item size, or else we can't query sub-item data
+                               item_data.emplace_back();
+                               item_data.emplace_back();
+                               group_item = make_shared<DecoderCollectionItem>(item_data, root_);
+                               root_->appendSubItem(group_item);
+                       }
+
+                       // Create decoder item
+                       item_data.clear();
+                       item_data.emplace_back(name);
+                       item_data.emplace_back(long_name);
+                       item_data.emplace_back(id);
+                       shared_ptr<DecoderCollectionItem> decoder_item =
+                               make_shared<DecoderCollectionItem>(item_data, group_item);
+
+                       // Add decoder to tag group
+                       group_item->appendSubItem(decoder_item);
+               }
+#endif
+       }
+}
+
+QVariant DecoderCollectionModel::data(const QModelIndex& index, int role) const
+{
+       if (!index.isValid())
+               return QVariant();
+
+       if (role == Qt::DisplayRole)
+       {
+               DecoderCollectionItem* item =
+                       static_cast<DecoderCollectionItem*>(index.internalPointer());
+
+               return item->data(index.column());
+       }
+
+       if ((role == Qt::FontRole) && (index.parent().isValid()) && (index.column() == 0))
+       {
+               QFont font;
+               font.setItalic(true);
+               return QVariant(font);
+       }
+
+       return QVariant();
+}
+
+Qt::ItemFlags DecoderCollectionModel::flags(const QModelIndex& index) const
+{
+       if (!index.isValid())
+               return nullptr;
+
+       return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+}
+
+QVariant DecoderCollectionModel::headerData(int section, Qt::Orientation orientation,
+       int role) const
+{
+       if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
+               return root_->data(section);
+
+       return QVariant();
+}
+
+QModelIndex DecoderCollectionModel::index(int row, int column,
+       const QModelIndex& parent_idx) const
+{
+       if (!hasIndex(row, column, parent_idx))
+               return QModelIndex();
+
+       DecoderCollectionItem* parent = root_.get();
+
+       if (parent_idx.isValid())
+               parent = static_cast<DecoderCollectionItem*>(parent_idx.internalPointer());
+
+       DecoderCollectionItem* subItem = parent->subItem(row).get();
+
+       return subItem ? createIndex(row, column, subItem) : QModelIndex();
+}
+
+QModelIndex DecoderCollectionModel::parent(const QModelIndex& index) const
+{
+       if (!index.isValid())
+               return QModelIndex();
+
+       DecoderCollectionItem* subItem =
+               static_cast<DecoderCollectionItem*>(index.internalPointer());
+
+       shared_ptr<DecoderCollectionItem> parent = subItem->parent();
+
+       return (parent == root_) ? QModelIndex() :
+               createIndex(parent->row(), 0, parent.get());
+}
+
+int DecoderCollectionModel::rowCount(const QModelIndex& parent_idx) const
+{
+       DecoderCollectionItem* parent = root_.get();
+
+       if (parent_idx.column() > 0)
+               return 0;
+
+       if (parent_idx.isValid())
+               parent = static_cast<DecoderCollectionItem*>(parent_idx.internalPointer());
+
+       return parent->subItemCount();
+}
+
+int DecoderCollectionModel::columnCount(const QModelIndex& parent_idx) const
+{
+       if (parent_idx.isValid())
+               return static_cast<DecoderCollectionItem*>(
+                       parent_idx.internalPointer())->columnCount();
+       else
+               return root_->columnCount();
+}
+
+
+} // namespace decoder_selector
+} // namespace subwindows
+} // namespace pv
diff --git a/pv/subwindows/decoder_selector/subwindow.cpp b/pv/subwindows/decoder_selector/subwindow.cpp
new file mode 100644 (file)
index 0000000..94ed6f4
--- /dev/null
@@ -0,0 +1,369 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2018 Soeren Apel <soeren@apelpie.net>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <algorithm>
+
+#include <QApplication>
+#include <QDebug>
+#include <QFontMetrics>
+#include <QInputDialog>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QScrollArea>
+#include <QVBoxLayout>
+
+#include "pv/session.hpp"
+#include "pv/subwindows/decoder_selector/subwindow.hpp"
+
+#include <libsigrokdecode/libsigrokdecode.h>
+#include "subwindow.hpp"  // Required only for lupdate since above include isn't recognized
+
+#define DECODERS_HAVE_TAGS \
+       ((SRD_PACKAGE_VERSION_MAJOR > 0) || \
+        (SRD_PACKAGE_VERSION_MAJOR == 0) && (SRD_PACKAGE_VERSION_MINOR > 5))
+
+using std::reverse;
+
+namespace pv {
+namespace subwindows {
+namespace decoder_selector {
+
+const char *initial_notice =
+       QT_TRANSLATE_NOOP("pv::subwindows::decoder_selector::SubWindow",
+                       "Select a decoder to see its description here.");  // clazy:exclude=non-pod-global-static
+
+const int min_width_margin = 75;
+
+
+bool QCustomSortFilterProxyModel::filterAcceptsRow(int source_row,
+       const QModelIndex& source_parent) const
+{
+       // Search model recursively
+
+       if (QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent))
+               return true;
+
+       const QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
+
+       for (int i = 0; i < sourceModel()->rowCount(index); i++)
+               if (filterAcceptsRow(i, index))
+                       return true;
+
+       return false;
+}
+
+
+void QCustomTreeView::currentChanged(const QModelIndex& current,
+       const QModelIndex& previous)
+{
+       QTreeView::currentChanged(current, previous);
+       currentChanged(current);
+}
+
+
+SubWindow::SubWindow(Session& session, QWidget* parent) :
+       SubWindowBase(session, parent),
+       splitter_(new QSplitter()),
+       tree_view_(new QCustomTreeView()),
+       info_box_(new QWidget()),
+       info_label_header_(new QLabel()),
+       info_label_body_(new QLabel()),
+       info_label_footer_(new QLabel()),
+       model_(new DecoderCollectionModel()),
+       sort_filter_model_(new QCustomSortFilterProxyModel())
+{
+       QVBoxLayout* root_layout = new QVBoxLayout(this);
+       root_layout->setContentsMargins(0, 0, 0, 0);
+       root_layout->addWidget(splitter_);
+
+       QWidget* upper_container = new QWidget();
+       QVBoxLayout* upper_layout = new QVBoxLayout(upper_container);
+       upper_layout->setContentsMargins(0, 5, 0, 0);
+       QLineEdit* filter = new QLineEdit();
+       upper_layout->addWidget(filter);
+       upper_layout->addWidget(tree_view_);
+
+       splitter_->setOrientation(Qt::Vertical);
+       splitter_->addWidget(upper_container);
+       splitter_->addWidget(info_box_);
+
+       const QIcon filter_icon(QIcon::fromTheme("search",
+               QIcon(":/icons/search.svg")));
+       filter->setClearButtonEnabled(true);
+       filter->addAction(filter_icon, QLineEdit::LeadingPosition);
+
+
+       sort_filter_model_->setSourceModel(model_);
+       sort_filter_model_->setSortCaseSensitivity(Qt::CaseInsensitive);
+       sort_filter_model_->setFilterCaseSensitivity(Qt::CaseInsensitive);
+       sort_filter_model_->setFilterKeyColumn(-1);
+
+       tree_view_->setModel(sort_filter_model_);
+       tree_view_->setRootIsDecorated(true);
+       tree_view_->setSortingEnabled(true);
+       tree_view_->sortByColumn(0, Qt::AscendingOrder);
+
+       // Hide the columns that hold the detailed item information
+       tree_view_->hideColumn(2);  // ID
+
+       // Ensure that all decoder tag names are fully visible by default
+       tree_view_->resizeColumnToContents(0);
+
+       tree_view_->setIndentation(10);
+
+#if (!DECODERS_HAVE_TAGS)
+       tree_view_->expandAll();
+       tree_view_->setItemsExpandable(false);
+#endif
+
+       QScrollArea* info_label_body_container = new QScrollArea();
+       info_label_body_container->setWidget(info_label_body_);
+       info_label_body_container->setWidgetResizable(true);
+
+       info_box_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+       QVBoxLayout* info_box_layout = new QVBoxLayout(info_box_);
+       info_box_layout->addWidget(info_label_header_);
+       info_box_layout->addWidget(info_label_body_container);
+       info_box_layout->addWidget(info_label_footer_);
+       info_box_layout->setAlignment(Qt::AlignTop);
+       Qt::TextInteractionFlags flags = Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard;
+       info_label_header_->setWordWrap(true);
+       info_label_header_->setTextInteractionFlags(flags);
+       info_label_body_->setWordWrap(true);
+       info_label_body_->setTextInteractionFlags(flags);
+       info_label_body_->setText(QString(tr(initial_notice)));
+       info_label_body_->setAlignment(Qt::AlignTop);
+       info_label_footer_->setWordWrap(true);
+       info_label_footer_->setTextInteractionFlags(flags);
+
+       connect(filter, SIGNAL(textChanged(const QString&)),
+               this, SLOT(on_filter_changed(const QString&)));
+       connect(filter, SIGNAL(returnPressed()),
+               this, SLOT(on_filter_return_pressed()));
+
+       connect(tree_view_, SIGNAL(currentChanged(const QModelIndex&)),
+               this, SLOT(on_item_changed(const QModelIndex&)));
+       connect(tree_view_, SIGNAL(activated(const QModelIndex&)),
+               this, SLOT(on_item_activated(const QModelIndex&)));
+
+       connect(this, SIGNAL(new_decoders_selected(vector<const srd_decoder*>)),
+               &session, SLOT(on_new_decoders_selected(vector<const srd_decoder*>)));
+
+       // Place the keyboard cursor in the filter QLineEdit initially
+       filter->setFocus();
+}
+
+bool SubWindow::has_toolbar() const
+{
+       return true;
+}
+
+QToolBar* SubWindow::create_toolbar(QWidget *parent) const
+{
+       QToolBar* toolbar = new QToolBar(parent);
+
+       return toolbar;
+}
+
+int SubWindow::minimum_width() const
+{
+       QFontMetrics m(info_label_body_->font());
+       const int label_width = m.width(QString(tr(initial_notice)));
+
+       return label_width + min_width_margin;
+}
+
+vector<const char*> SubWindow::get_decoder_inputs(const srd_decoder* d) const
+{
+       vector<const char*> ret_val;
+
+       for (GSList* li = d->inputs; li; li = li->next)
+               ret_val.push_back((const char*)li->data);
+
+       return ret_val;
+}
+
+vector<const srd_decoder*> SubWindow::get_decoders_providing(const char* output) const
+{
+       vector<const srd_decoder*> ret_val;
+
+       for (GSList* li = (GSList*)srd_decoder_list(); li; li = li->next) {
+               const srd_decoder* d = (srd_decoder*)li->data;
+               assert(d);
+
+               if (!d->outputs)
+                       continue;
+
+               const int maxlen = 1024;
+
+               // TODO For now we ignore that d->outputs is actually a list
+               if (strncmp((char*)(d->outputs->data), output, maxlen) == 0)
+                       ret_val.push_back(d);
+       }
+
+       return ret_val;
+}
+
+void SubWindow::on_item_changed(const QModelIndex& index)
+{
+       QString decoder_name, id, longname, desc, doc, tags;
+
+       // If the parent isn't valid, a category title was clicked
+       if (index.isValid() && index.parent().isValid()) {
+               QModelIndex id_index = index.model()->index(index.row(), 2, index.parent());
+               decoder_name = index.model()->data(id_index, Qt::DisplayRole).toString();
+
+               if (decoder_name.isEmpty())
+                       return;
+
+               const srd_decoder* d = srd_decoder_get_by_id(decoder_name.toUtf8());
+
+               id = QString::fromUtf8(d->id);
+               longname = QString::fromUtf8(d->longname);
+               desc = QString::fromUtf8(d->desc);
+               doc = QString::fromUtf8(srd_decoder_doc_get(d)).trimmed();
+
+#if DECODERS_HAVE_TAGS
+               for (GSList* li = (GSList*)d->tags; li; li = li->next) {
+                       QString s = (li == (GSList*)d->tags) ?
+                               tr((char*)li->data) :
+                               QString(tr(", %1")).arg(tr((char*)li->data));
+                       tags.append(s);
+               }
+#endif
+       } else
+               doc = QString(tr(initial_notice));
+
+       if (!id.isEmpty())
+               info_label_header_->setText(
+                       QString("<span style='font-size:large'><b>%1 (%2)</b></span><br><i>%3</i>")
+                       .arg(longname, id, desc));
+       else
+               info_label_header_->clear();
+
+       info_label_body_->setText(doc);
+
+       if (!tags.isEmpty())
+               info_label_footer_->setText(tr("<p align='right'>Tags: %1</p>").arg(tags));
+       else
+               info_label_footer_->clear();
+}
+
+void SubWindow::on_item_activated(const QModelIndex& index)
+{
+       if (!index.isValid())
+               return;
+
+       QModelIndex id_index = index.model()->index(index.row(), 2, index.parent());
+       QString decoder_name = index.model()->data(id_index, Qt::DisplayRole).toString();
+
+       const srd_decoder* chosen_decoder = srd_decoder_get_by_id(decoder_name.toUtf8());
+       if (chosen_decoder == nullptr)
+               return;
+
+       vector<const srd_decoder*> decoders;
+       decoders.push_back(chosen_decoder);
+
+       // If the decoder only depends on logic inputs, we add it and are done
+       vector<const char*> inputs = get_decoder_inputs(decoders.front());
+       if (inputs.size() == 0) {
+               qWarning() << "Protocol decoder" << decoder_name << "cannot have 0 inputs!";
+               return;
+       }
+
+       if (strcmp(inputs.at(0), "logic") == 0) {
+               new_decoders_selected(decoders);
+               return;
+       }
+
+       // Check if we can automatically fulfill the stacking requirements
+       while (strcmp(inputs.at(0), "logic") != 0) {
+               vector<const srd_decoder*> prov_decoders = get_decoders_providing(inputs.at(0));
+
+               if (prov_decoders.size() == 0) {
+                       // Emit warning and add the stack that we could gather so far
+                       qWarning() << "Protocol decoder" << QString::fromUtf8(decoders.back()->id) \
+                               << "has input that no other decoder provides:" << QString::fromUtf8(inputs.at(0));
+                       break;
+               }
+
+               if (prov_decoders.size() == 1) {
+                       decoders.push_back(prov_decoders.front());
+               } else {
+                       // Let user decide which one to use
+                       QString caption = QString(tr("Protocol decoder <b>%1</b> requires input type <b>%2</b> " \
+                               "which several decoders provide.<br>Choose which one to use:<br>"))
+                                       .arg(QString::fromUtf8(decoders.back()->id), QString::fromUtf8(inputs.at(0)));
+
+                       QStringList items;
+                       for (const srd_decoder* d : prov_decoders)
+                               items << QString::fromUtf8(d->id) + " (" + QString::fromUtf8(d->longname) + ")";
+                       bool ok_clicked;
+                       QString item = QInputDialog::getItem(this, tr("Choose Decoder"),
+                               tr(caption.toUtf8()), items, 0, false, &ok_clicked);
+
+                       if ((!ok_clicked) || (item.isEmpty()))
+                               return;
+
+                       QString d = item.section(' ', 0, 0);
+                       decoders.push_back(srd_decoder_get_by_id(d.toUtf8()));
+               }
+
+               inputs = get_decoder_inputs(decoders.back());
+       }
+
+       // Reverse decoder list and add the stack
+       reverse(decoders.begin(), decoders.end());
+       new_decoders_selected(decoders);
+}
+
+void SubWindow::on_filter_changed(const QString& text)
+{
+       sort_filter_model_->setFilterFixedString(text);
+
+       // Expand the "All Decoders" category/tag if the user filtered
+       tree_view_->setExpanded(tree_view_->model()->index(0, 0), !text.isEmpty());
+}
+
+void SubWindow::on_filter_return_pressed()
+{
+       int num_visible_decoders = 0;
+       QModelIndex last_valid_index;
+
+       QModelIndex index = tree_view_->model()->index(0, 0);
+
+       while (index.isValid()) {
+               QModelIndex id_index = index.model()->index(index.row(), 2, index.parent());
+               QString decoder_name = index.model()->data(id_index, Qt::DisplayRole).toString();
+               if (!decoder_name.isEmpty()) {
+                       last_valid_index = index;
+                       num_visible_decoders++;
+               }
+               index = tree_view_->indexBelow(index);
+       }
+
+       // If only one decoder matches the filter, apply it when the user presses enter
+       if (num_visible_decoders == 1)
+               tree_view_->activated(last_valid_index);
+}
+
+} // namespace decoder_selector
+} // namespace subwindows
+} // namespace pv
diff --git a/pv/subwindows/decoder_selector/subwindow.hpp b/pv/subwindows/decoder_selector/subwindow.hpp
new file mode 100644 (file)
index 0000000..c189fb2
--- /dev/null
@@ -0,0 +1,155 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2018 Soeren Apel <soeren@apelpie.net>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef PULSEVIEW_PV_SUBWINDOWS_DECODERSELECTOR_SUBWINDOW_HPP
+#define PULSEVIEW_PV_SUBWINDOWS_DECODERSELECTOR_SUBWINDOW_HPP
+
+#include <vector>
+
+#include <QAbstractItemModel>
+#include <QLabel>
+#include <QSortFilterProxyModel>
+#include <QSplitter>
+#include <QTreeView>
+
+#include "pv/subwindows/subwindowbase.hpp"
+
+using std::shared_ptr;
+
+namespace pv {
+namespace subwindows {
+namespace decoder_selector {
+
+class DecoderCollectionItem
+{
+public:
+       DecoderCollectionItem(const vector<QVariant>& data,
+               shared_ptr<DecoderCollectionItem> parent = nullptr);
+
+       void appendSubItem(shared_ptr<DecoderCollectionItem> item);
+
+       shared_ptr<DecoderCollectionItem> subItem(int row) const;
+       shared_ptr<DecoderCollectionItem> parent() const;
+       shared_ptr<DecoderCollectionItem> findSubItem(const QVariant& value, int column);
+
+       int subItemCount() const;
+       int columnCount() const;
+       int row() const;
+       QVariant data(int column) const;
+
+private:
+       vector< shared_ptr<DecoderCollectionItem> > subItems_;
+       vector<QVariant> data_;
+       shared_ptr<DecoderCollectionItem> parent_;
+};
+
+
+class DecoderCollectionModel : public QAbstractItemModel
+{
+       Q_OBJECT
+
+public:
+       DecoderCollectionModel(QObject* parent = nullptr);
+
+       QVariant data(const QModelIndex& index, int role) const override;
+       Qt::ItemFlags flags(const QModelIndex& index) const override;
+
+       QVariant headerData(int section, Qt::Orientation orientation,
+               int role = Qt::DisplayRole) const override;
+       QModelIndex index(int row, int column,
+               const QModelIndex& parent_idx = QModelIndex()) const override;
+
+       QModelIndex parent(const QModelIndex& index) const override;
+
+       int rowCount(const QModelIndex& parent_idx = QModelIndex()) const override;
+       int columnCount(const QModelIndex& parent_idx = QModelIndex()) const override;
+
+private:
+       shared_ptr<DecoderCollectionItem> root_;
+};
+
+
+class QCustomSortFilterProxyModel : public QSortFilterProxyModel
+{
+protected:
+       bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const;
+};
+
+class QCustomTreeView : public QTreeView
+{
+       Q_OBJECT
+
+public:
+       void currentChanged(const QModelIndex& current, const QModelIndex& previous);
+
+Q_SIGNALS:
+       void currentChanged(const QModelIndex& current);
+};
+
+class SubWindow : public SubWindowBase
+{
+       Q_OBJECT
+
+public:
+       explicit SubWindow(Session &session, QWidget *parent = nullptr);
+
+       bool has_toolbar() const;
+       QToolBar* create_toolbar(QWidget *parent) const;
+
+       int minimum_width() const;
+
+       /**
+        * Returns a list of input types that a given protocol decoder requires
+        * ("logic", "uart", etc.)
+        */
+       vector<const char*> get_decoder_inputs(const srd_decoder* d) const;
+
+       /**
+        * Returns a list of protocol decoder IDs which provide a given output
+        * ("uart", "spi", etc.)
+        */
+       vector<const srd_decoder*> get_decoders_providing(const char* output) const;
+
+Q_SIGNALS:
+       void new_decoders_selected(vector<const srd_decoder*> decoders);
+
+public Q_SLOTS:
+       void on_item_changed(const QModelIndex& index);
+       void on_item_activated(const QModelIndex& index);
+
+       void on_filter_changed(const QString& text);
+       void on_filter_return_pressed();
+
+private:
+       QSplitter* splitter_;
+       QCustomTreeView* tree_view_;
+       QWidget* info_box_;
+       QLabel* info_label_header_;
+       QLabel* info_label_body_;
+       QLabel* info_label_footer_;
+       DecoderCollectionModel* model_;
+       QCustomSortFilterProxyModel* sort_filter_model_;
+};
+
+} // namespace decoder_selector
+} // namespace subwindows
+} // namespace pv
+
+#endif // PULSEVIEW_PV_SUBWINDOWS_DECODERSELECTOR_SUBWINDOW_HPP
+
diff --git a/pv/subwindows/subwindowbase.cpp b/pv/subwindows/subwindowbase.cpp
new file mode 100644 (file)
index 0000000..f81b358
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2018 Soeren Apel <soeren@apelpie.net>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef ENABLE_DECODE
+#include <libsigrokdecode/libsigrokdecode.h>
+#endif
+
+#include <QWidget>
+
+#include "pv/session.hpp"
+#include "pv/subwindows/subwindowbase.hpp"
+
+using std::shared_ptr;
+
+namespace pv {
+namespace subwindows {
+
+SubWindowBase::SubWindowBase(Session &session, QWidget *parent) :
+       QWidget(parent),
+       session_(session)
+{
+       connect(&session_, SIGNAL(signals_changed()), this, SLOT(on_signals_changed()));
+}
+
+bool SubWindowBase::has_toolbar() const
+{
+       return false;
+}
+
+QToolBar* SubWindowBase::create_toolbar(QWidget *parent) const
+{
+       (void)parent;
+
+       return nullptr;
+}
+
+Session& SubWindowBase::session()
+{
+       return session_;
+}
+
+const Session& SubWindowBase::session() const
+{
+       return session_;
+}
+
+unordered_set< shared_ptr<data::SignalBase> > SubWindowBase::signalbases() const
+{
+       return signalbases_;
+}
+
+void SubWindowBase::clear_signalbases()
+{
+       for (const shared_ptr<data::SignalBase>& signalbase : signalbases_) {
+               disconnect(signalbase.get(), SIGNAL(samples_cleared()),
+                       this, SLOT(on_data_updated()));
+               disconnect(signalbase.get(), SIGNAL(samples_added(uint64_t, uint64_t, uint64_t)),
+                       this, SLOT(on_samples_added(uint64_t, uint64_t, uint64_t)));
+       }
+
+       signalbases_.clear();
+}
+
+void SubWindowBase::add_signalbase(const shared_ptr<data::SignalBase> signalbase)
+{
+       signalbases_.insert(signalbase);
+}
+
+#ifdef ENABLE_DECODE
+void SubWindowBase::clear_decode_signals()
+{
+}
+
+void SubWindowBase::add_decode_signal(shared_ptr<data::DecodeSignal> signal)
+{
+       (void)signal;
+}
+
+void SubWindowBase::remove_decode_signal(shared_ptr<data::DecodeSignal> signal)
+{
+       (void)signal;
+}
+#endif
+
+int SubWindowBase::minimum_width() const
+{
+       return 0;
+}
+
+void SubWindowBase::on_signals_changed()
+{
+}
+
+} // namespace subwindows
+} // namespace pv
diff --git a/pv/subwindows/subwindowbase.hpp b/pv/subwindows/subwindowbase.hpp
new file mode 100644 (file)
index 0000000..ab3a08b
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2018 Soeren Apel <soeren@apelpie.net>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef PULSEVIEW_PV_SUBWINDOWBASE_HPP
+#define PULSEVIEW_PV_SUBWINDOWBASE_HPP
+
+#include <cstdint>
+#include <memory>
+#include <unordered_set>
+
+#include <QToolBar>
+#include <QWidget>
+
+#include <pv/data/signalbase.hpp>
+
+#ifdef ENABLE_DECODE
+#include <pv/data/decodesignal.hpp>
+#endif
+
+using std::shared_ptr;
+using std::unordered_set;
+
+namespace pv {
+
+class Session;
+
+namespace subwindows {
+
+enum SubWindowType {
+       SubWindowTypeDecoderSelector,
+};
+
+class SubWindowBase : public QWidget
+{
+       Q_OBJECT
+
+public:
+       explicit SubWindowBase(Session &session, QWidget *parent = nullptr);
+
+       virtual bool has_toolbar() const;
+       virtual QToolBar* create_toolbar(QWidget *parent) const;
+
+       Session& session();
+       const Session& session() const;
+
+       /**
+        * Returns the signal bases contained in this view.
+        */
+       unordered_set< shared_ptr<data::SignalBase> > signalbases() const;
+
+       virtual void clear_signalbases();
+
+       virtual void add_signalbase(const shared_ptr<data::SignalBase> signalbase);
+
+#ifdef ENABLE_DECODE
+       virtual void clear_decode_signals();
+
+       virtual void add_decode_signal(shared_ptr<data::DecodeSignal> signal);
+
+       virtual void remove_decode_signal(shared_ptr<data::DecodeSignal> signal);
+#endif
+
+       virtual int minimum_width() const;
+
+public Q_SLOTS:
+       virtual void on_signals_changed();
+
+protected:
+       Session &session_;
+
+       unordered_set< shared_ptr<data::SignalBase> > signalbases_;
+};
+
+} // namespace subwindows
+} // namespace pv
+
+#endif // PULSEVIEW_PV_SUBWINDOWBASE_HPP
index e6beb2b3ba704e198c8aff0076787a9d96798fe9..a7998b4046511077502a4971bdfa7bc55d3e21d0 100644 (file)
@@ -35,7 +35,6 @@
 
 #include <boost/algorithm/string/join.hpp>
 
-#include <pv/data/decodesignal.hpp>
 #include <pv/devicemanager.hpp>
 #include <pv/devices/hardwaredevice.hpp>
 #include <pv/devices/inputfile.hpp>
@@ -51,7 +50,7 @@
 #include <pv/widgets/exportmenu.hpp>
 #include <pv/widgets/importmenu.hpp>
 #ifdef ENABLE_DECODE
-#include <pv/widgets/decodermenu.hpp>
+#include <pv/data/decodesignal.hpp>
 #endif
 
 #include <libsigrokcxx/libsigrokcxx.hpp>
@@ -93,7 +92,10 @@ MainBar::MainBar(Session &session, QWidget *parent, pv::views::trace::View *view
        action_open_(new QAction(this)),
        action_save_as_(new QAction(this)),
        action_save_selection_as_(new QAction(this)),
+       action_restore_setup_(new QAction(this)),
+       action_save_setup_(new QAction(this)),
        action_connect_(new QAction(this)),
+       new_view_button_(new QToolButton()),
        open_button_(new QToolButton()),
        save_button_(new QToolButton()),
        device_selector_(parent, session.device_manager(), action_connect_),
@@ -107,8 +109,7 @@ MainBar::MainBar(Session &session, QWidget *parent, pv::views::trace::View *view
        updating_sample_count_(false),
        sample_count_supported_(false)
 #ifdef ENABLE_DECODE
-       , add_decoder_button_(new QToolButton()),
-       menu_decoders_add_(new pv::widgets::DecoderMenu(this, true))
+       , add_decoder_button_(new QToolButton())
 #endif
 {
        setObjectName(QString::fromUtf8("MainBar"));
@@ -129,6 +130,10 @@ MainBar::MainBar(Session &session, QWidget *parent, pv::views::trace::View *view
        connect(action_open_, SIGNAL(triggered(bool)),
                this, SLOT(on_actionOpen_triggered()));
 
+       action_restore_setup_->setText(tr("Restore Session Setu&p..."));
+       connect(action_restore_setup_, SIGNAL(triggered(bool)),
+               this, SLOT(on_actionRestoreSetup_triggered()));
+
        action_save_as_->setText(tr("&Save As..."));
        action_save_as_->setIcon(QIcon::fromTheme("document-save-as",
                QIcon(":/icons/document-save-as.png")));
@@ -143,6 +148,10 @@ MainBar::MainBar(Session &session, QWidget *parent, pv::views::trace::View *view
        connect(action_save_selection_as_, SIGNAL(triggered(bool)),
                this, SLOT(on_actionSaveSelectionAs_triggered()));
 
+       action_save_setup_->setText(tr("Save Session Setu&p..."));
+       connect(action_save_setup_, SIGNAL(triggered(bool)),
+               this, SLOT(on_actionSaveSetup_triggered()));
+
        widgets::ExportMenu *menu_file_export = new widgets::ExportMenu(this,
                session.device_manager().context());
        menu_file_export->setTitle(tr("&Export"));
@@ -159,9 +168,30 @@ MainBar::MainBar(Session &session, QWidget *parent, pv::views::trace::View *view
        connect(action_connect_, SIGNAL(triggered(bool)),
                this, SLOT(on_actionConnect_triggered()));
 
+       // New view button
+       QMenu *menu_new_view = new QMenu();
+       connect(menu_new_view, SIGNAL(triggered(QAction*)),
+               this, SLOT(on_actionNewView_triggered(QAction*)));
+
+       for (int i = 0; i < views::ViewTypeCount; i++) {
+               QAction *const action = menu_new_view->addAction(tr(views::ViewTypeNames[i]));
+               action->setData(qVariantFromValue(i));
+       }
+
+       new_view_button_->setMenu(menu_new_view);
+       new_view_button_->setDefaultAction(action_new_view_);
+       new_view_button_->setPopupMode(QToolButton::MenuButtonPopup);
+
        // Open button
+       vector<QAction*> open_actions;
+       open_actions.push_back(action_open_);
+       QAction* separator_o = new QAction(this);
+       separator_o->setSeparator(true);
+       open_actions.push_back(separator_o);
+       open_actions.push_back(action_restore_setup_);
+
        widgets::ImportMenu *import_menu = new widgets::ImportMenu(this,
-               session.device_manager().context(), action_open_);
+               session.device_manager().context(), open_actions);
        connect(import_menu, SIGNAL(format_selected(shared_ptr<sigrok::InputFormat>)),
                this, SLOT(import_file(shared_ptr<sigrok::InputFormat>)));
 
@@ -170,12 +200,16 @@ MainBar::MainBar(Session &session, QWidget *parent, pv::views::trace::View *view
        open_button_->setPopupMode(QToolButton::MenuButtonPopup);
 
        // Save button
-       vector<QAction*> open_actions;
-       open_actions.push_back(action_save_as_);
-       open_actions.push_back(action_save_selection_as_);
+       vector<QAction*> save_actions;
+       save_actions.push_back(action_save_as_);
+       save_actions.push_back(action_save_selection_as_);
+       QAction* separator_s = new QAction(this);
+       separator_s->setSeparator(true);
+       save_actions.push_back(separator_s);
+       save_actions.push_back(action_save_setup_);
 
        widgets::ExportMenu *export_menu = new widgets::ExportMenu(this,
-               session.device_manager().context(), open_actions);
+               session.device_manager().context(), save_actions);
        connect(export_menu, SIGNAL(format_selected(shared_ptr<sigrok::OutputFormat>)),
                this, SLOT(export_file(shared_ptr<sigrok::OutputFormat>)));
 
@@ -189,14 +223,13 @@ MainBar::MainBar(Session &session, QWidget *parent, pv::views::trace::View *view
 
        // Setup the decoder button
 #ifdef ENABLE_DECODE
-       menu_decoders_add_->setTitle(tr("&Add"));
-       connect(menu_decoders_add_, SIGNAL(decoder_selected(srd_decoder*)),
-               this, SLOT(add_decoder(srd_decoder*)));
-
        add_decoder_button_->setIcon(QIcon(":/icons/add-decoder.svg"));
        add_decoder_button_->setPopupMode(QToolButton::InstantPopup);
-       add_decoder_button_->setMenu(menu_decoders_add_);
-       add_decoder_button_->setToolTip(tr("Add low-level, non-stacked protocol decoder"));
+       add_decoder_button_->setToolTip(tr("Add protocol decoder"));
+       add_decoder_button_->setShortcut(QKeySequence(Qt::Key_D));
+
+       connect(add_decoder_button_, SIGNAL(clicked()),
+               this, SLOT(on_add_decoder_clicked()));
 #endif
 
        connect(&sample_count_, SIGNAL(value_changed()),
@@ -556,25 +589,12 @@ void MainBar::commit_sample_count()
 void MainBar::show_session_error(const QString text, const QString info_text)
 {
        QMessageBox msg(this);
-       msg.setText(text);
-       msg.setInformativeText(info_text);
+       msg.setText(text + "\n\n" + info_text);
        msg.setStandardButtons(QMessageBox::Ok);
        msg.setIcon(QMessageBox::Warning);
        msg.exec();
 }
 
-void MainBar::add_decoder(srd_decoder *decoder)
-{
-#ifdef ENABLE_DECODE
-       assert(decoder);
-       shared_ptr<data::DecodeSignal> signal = session_.add_decode_signal();
-       if (signal)
-               signal->stack_decoder(decoder);
-#else
-       (void)decoder;
-#endif
-}
-
 void MainBar::export_file(shared_ptr<OutputFormat> format, bool selection_only)
 {
        using pv::dialogs::StoreProgress;
@@ -701,7 +721,7 @@ void MainBar::import_file(shared_ptr<InputFormat> format)
                options = dlg.options();
        }
 
-       session_.load_file(file_name, format, options);
+       session_.load_file(file_name, "", format, options);
 
        const QString abs_path = QFileInfo(file_name).absolutePath();
        settings.setValue(SettingOpenDirectory, abs_path);
@@ -750,9 +770,13 @@ void MainBar::on_config_changed()
        commit_sample_rate();
 }
 
-void MainBar::on_actionNewView_triggered()
+void MainBar::on_actionNewView_triggered(QAction* action)
 {
-       new_view(&session_);
+       if (action)
+               new_view(&session_, action->data().toInt());
+       else
+               // When the icon of the button is clicked, we create a trace view
+               new_view(&session_, views::ViewTypeTrace);
 }
 
 void MainBar::on_actionOpen_triggered()
@@ -784,6 +808,40 @@ void MainBar::on_actionSaveSelectionAs_triggered()
        export_file(session_.device_manager().context()->output_formats()["srzip"], true);
 }
 
+void MainBar::on_actionSaveSetup_triggered()
+{
+       QSettings settings;
+       const QString dir = settings.value(SettingSaveDirectory).toString();
+
+       const QString file_name = QFileDialog::getSaveFileName(
+               this, tr("Save File"), dir, tr(
+                               "PulseView Session Setups (*.pvs);;"
+                               "All Files (*)"));
+
+       if (file_name.isEmpty())
+               return;
+
+       QSettings settings_storage(file_name, QSettings::IniFormat);
+       session_.save_setup(settings_storage);
+}
+
+void MainBar::on_actionRestoreSetup_triggered()
+{
+       QSettings settings;
+       const QString dir = settings.value(SettingSaveDirectory).toString();
+
+       const QString file_name = QFileDialog::getOpenFileName(
+               this, tr("Open File"), dir, tr(
+                               "PulseView Session Setups (*.pvs);;"
+                               "All Files (*)"));
+
+       if (file_name.isEmpty())
+               return;
+
+       QSettings settings_storage(file_name, QSettings::IniFormat);
+       session_.restore_setup(settings_storage);
+}
+
 void MainBar::on_actionConnect_triggered()
 {
        // Stop any currently running capture session
@@ -799,9 +857,14 @@ void MainBar::on_actionConnect_triggered()
        update_device_list();
 }
 
+void MainBar::on_add_decoder_clicked()
+{
+       show_decoder_selector(&session_);
+}
+
 void MainBar::add_toolbar_widgets()
 {
-       addAction(action_new_view_);
+       addWidget(new_view_button_);
        addSeparator();
        addWidget(open_button_);
        addWidget(save_button_);
index e938dbbc01d1dc4c877be7bb7e42b07a460fd5f1..04c344c5351088e8e636e50159adb1c536d3dc08 100644 (file)
@@ -98,6 +98,8 @@ public:
        QAction* action_open() const;
        QAction* action_save_as() const;
        QAction* action_save_selection_as() const;
+       QAction* action_restore_setup() const;
+       QAction* action_save_setup() const;
        QAction* action_connect() const;
 
 private:
@@ -114,17 +116,9 @@ private:
        void commit_sample_rate();
        void commit_sample_count();
 
-       QAction *const action_new_view_;
-       QAction *const action_open_;
-       QAction *const action_save_as_;
-       QAction *const action_save_selection_as_;
-       QAction *const action_connect_;
-
 private Q_SLOTS:
        void show_session_error(const QString text, const QString info_text);
 
-       void add_decoder(srd_decoder *decoder);
-
        void export_file(shared_ptr<sigrok::OutputFormat> format,
                bool selection_only = false);
        void import_file(shared_ptr<sigrok::InputFormat> format);
@@ -137,24 +131,38 @@ private Q_SLOTS:
 
        void on_config_changed();
 
-       void on_actionNewView_triggered();
+       void on_actionNewView_triggered(QAction* action = nullptr);
 
        void on_actionOpen_triggered();
        void on_actionSaveAs_triggered();
        void on_actionSaveSelectionAs_triggered();
 
+       void on_actionSaveSetup_triggered();
+       void on_actionRestoreSetup_triggered();
+
        void on_actionConnect_triggered();
 
+       void on_add_decoder_clicked();
+
 protected:
        void add_toolbar_widgets();
 
        bool eventFilter(QObject *watched, QEvent *event);
 
 Q_SIGNALS:
-       void new_view(Session *session);
+       void new_view(Session *session, int type);
+       void show_decoder_selector(Session *session);
 
 private:
-       QToolButton *open_button_, *save_button_;
+       QAction *const action_new_view_;
+       QAction *const action_open_;
+       QAction *const action_save_as_;
+       QAction *const action_save_selection_as_;
+       QAction *const action_restore_setup_;
+       QAction *const action_save_setup_;
+       QAction *const action_connect_;
+
+       QToolButton *new_view_button_, *open_button_, *save_button_;
 
        pv::widgets::DeviceToolButton device_selector_;
 
@@ -173,7 +181,6 @@ private:
 
 #ifdef ENABLE_DECODE
        QToolButton *add_decoder_button_;
-       QMenu *const menu_decoders_add_;
 #endif
 };
 
index 49b9467c1642737a1a995df5adcb622e2bbddfa1..9a9a5065a2e17180eb6cdebb71b4aefc07fb5833 100644 (file)
@@ -138,8 +138,8 @@ QString format_time_si(const Timestamp& v, SIPrefix prefix,
        QTextStream ts(&s);
        if (sign && !v.is_zero())
                ts << forcesign;
-       ts << qSetRealNumberPrecision(precision) << (v * multiplier) << ' '
-               << prefix << unit;
+       ts << qSetRealNumberPrecision(precision) << (v * multiplier);
+       ts << ' ' << prefix << unit;
 
        return s;
 }
@@ -161,6 +161,10 @@ QString format_value_si(double v, SIPrefix prefix, unsigned precision,
                                exp -= 3;
                        }
                }
+
+               const int prefix_order = -exponent(prefix);
+               precision = (prefix >= SIPrefix::none) ? max((int)(precision + prefix_order), 0) :
+                       max((int)(precision - prefix_order), 0);
        }
 
        assert(prefix >= SIPrefix::yocto);
index dd7be222b073760b2ec5392ee222617d9717f869..e1640c4a374d8a9aed51231ae03a6b1c895486b1 100644 (file)
@@ -38,6 +38,7 @@ namespace pv {
 namespace util {
 
 enum class TimeUnit {
+       None = 0,
        Time = 1,
        Samples = 2
 };
diff --git a/pv/views/decoder_binary/QHexView.cpp b/pv/views/decoder_binary/QHexView.cpp
new file mode 100644 (file)
index 0000000..cdaf1b5
--- /dev/null
@@ -0,0 +1,689 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 Victor Anjin <virinext@gmail.com>
+ * Copyright (C) 2019 Soeren Apel <soeren@apelpie.net>
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include <QApplication>
+#include <QClipboard>
+#include <QDebug>
+#include <QFont>
+#include <QKeyEvent>
+#include <QScrollBar>
+#include <QSize>
+#include <QPainter>
+#include <QPaintEvent>
+
+#include "QHexView.hpp"
+
+const unsigned int BYTES_PER_LINE   = 16;
+const unsigned int HEXCHARS_IN_LINE = BYTES_PER_LINE * 3 - 1;
+const unsigned int GAP_ADR_HEX      = 10;
+const unsigned int GAP_HEX_ASCII    = 10;
+const unsigned int GAP_ASCII_SLIDER = 5;
+
+
+QHexView::QHexView(QWidget *parent):
+       QAbstractScrollArea(parent),
+       mode_(ChunkedDataMode),
+       data_(nullptr),
+       selectBegin_(0),
+       selectEnd_(0),
+       cursorPos_(0)
+{
+       setFont(QFont("Courier", 10));
+
+       charWidth_ = fontMetrics().boundingRect('X').width();
+       charHeight_ = fontMetrics().height();
+
+       // Determine X coordinates of the three sub-areas
+       posAddr_  = 0;
+       posHex_   = 10 * charWidth_ + GAP_ADR_HEX;
+       posAscii_ = posHex_ + HEXCHARS_IN_LINE * charWidth_ + GAP_HEX_ASCII;
+
+       setFocusPolicy(Qt::StrongFocus);
+
+       if (palette().color(QPalette::ButtonText).toHsv().value() > 127) {
+               // Color is bright
+               chunk_colors_.emplace_back(100, 149, 237); // QColorConstants::Svg::cornflowerblue
+               chunk_colors_.emplace_back(60, 179, 113);  // QColorConstants::Svg::mediumseagreen
+               chunk_colors_.emplace_back(210, 180, 140); // QColorConstants::Svg::tan
+       } else {
+               // Color is dark
+               chunk_colors_.emplace_back(0, 0, 139);   // QColorConstants::Svg::darkblue
+               chunk_colors_.emplace_back(34, 139, 34); // QColorConstants::Svg::forestgreen
+               chunk_colors_.emplace_back(160, 82, 45); // QColorConstants::Svg::sienna
+       }
+}
+
+void QHexView::set_mode(Mode m)
+{
+       mode_ = m;
+
+       // This is not expected to be set when data is showing,
+       // so we don't update the viewport here
+}
+
+void QHexView::set_data(const DecodeBinaryClass* data)
+{
+       data_ = data;
+
+       size_t size = 0;
+       if (data) {
+               size_t chunks = data_->chunks.size();
+               for (size_t i = 0; i < chunks; i++)
+                       size += data_->chunks[i].data.size();
+       }
+       data_size_ = size;
+
+       viewport()->update();
+}
+
+unsigned int QHexView::get_bytes_per_line() const
+{
+       return BYTES_PER_LINE;
+}
+
+void QHexView::clear()
+{
+       verticalScrollBar()->setValue(0);
+       data_ = nullptr;
+       data_size_ = 0;
+
+       viewport()->update();
+}
+
+void QHexView::showFromOffset(size_t offset)
+{
+       if (data_ && (offset < data_size_)) {
+               setCursorPos(offset * 2);
+
+               int cursorY = cursorPos_ / (2 * BYTES_PER_LINE);
+               verticalScrollBar() -> setValue(cursorY);
+       }
+
+       viewport()->update();
+}
+
+QSizePolicy QHexView::sizePolicy() const
+{
+       return QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
+}
+
+pair<size_t, size_t> QHexView::get_selection() const
+{
+       size_t start = selectBegin_ / 2;
+       size_t end = selectEnd_ / 2;
+
+       if (start == end) {
+               // Nothing is currently selected
+               start = 0;
+               end = data_size_;
+       } if (end < data_size_)
+               end++;
+
+       return std::make_pair(start, end);
+}
+
+size_t QHexView::create_hex_line(size_t start, size_t end, QString* dest,
+       bool with_offset, bool with_ascii)
+{
+       dest->clear();
+
+       // Determine start address for the row
+       uint64_t row = start / BYTES_PER_LINE;
+       uint64_t offset = row * BYTES_PER_LINE;
+       end = std::min((uint64_t)end, offset + BYTES_PER_LINE);
+
+       if (with_offset)
+               dest->append(QString("%1 ").arg(row * BYTES_PER_LINE, 10, 16, QChar('0')).toUpper());
+
+       initialize_byte_iterator(offset);
+       for (size_t i = offset; i < offset + BYTES_PER_LINE; i++) {
+               uint8_t value = 0;
+
+               if (i < end)
+                       value = get_next_byte();
+
+               if ((i < start) || (i >= end))
+                       dest->append("   ");
+               else
+                       dest->append(QString("%1 ").arg(value, 2, 16, QChar('0')).toUpper());
+       }
+
+       if (with_ascii) {
+               initialize_byte_iterator(offset);
+               for (size_t i = offset; i < end; i++) {
+                       uint8_t value = get_next_byte();
+
+                       if ((value < 0x20) || (value > 0x7E))
+                               value = '.';
+
+                       if (i < start)
+                               dest->append(' ');
+                       else
+                               dest->append((char)value);
+               }
+       }
+
+       return end;
+}
+
+void QHexView::initialize_byte_iterator(size_t offset)
+{
+       current_chunk_id_ = 0;
+       current_chunk_offset_ = 0;
+       current_offset_ = offset;
+
+       size_t chunks = data_->chunks.size();
+       for (size_t i = 0; i < chunks; i++) {
+               size_t size = data_->chunks[i].data.size();
+
+               if (offset >= size) {
+                       current_chunk_id_++;
+                       offset -= size;
+               } else {
+                       current_chunk_offset_ = offset;
+                       break;
+               }
+       }
+
+       if (current_chunk_id_ < data_->chunks.size())
+               current_chunk_ = data_->chunks[current_chunk_id_];
+}
+
+uint8_t QHexView::get_next_byte(bool* is_next_chunk)
+{
+       if (is_next_chunk != nullptr)
+               *is_next_chunk = (current_chunk_offset_ == 0);
+
+       uint8_t v = 0;
+       if (current_chunk_offset_ < current_chunk_.data.size())
+               v = current_chunk_.data[current_chunk_offset_];
+
+       current_offset_++;
+       current_chunk_offset_++;
+
+       if (current_offset_ > data_size_) {
+               qWarning() << "QHexView::get_next_byte() overran binary data boundary:" <<
+                       current_offset_ << "of" << data_size_ << "bytes";
+               return 0xEE;
+       }
+
+       if ((current_chunk_offset_ == current_chunk_.data.size()) && (current_offset_ < data_size_)) {
+               current_chunk_id_++;
+               current_chunk_offset_ = 0;
+               current_chunk_ = data_->chunks[current_chunk_id_];
+       }
+
+       return v;
+}
+
+QSize QHexView::getFullSize() const
+{
+       size_t width = posAscii_ + (BYTES_PER_LINE * charWidth_);
+
+       if (verticalScrollBar()->isEnabled())
+               width += GAP_ASCII_SLIDER + verticalScrollBar()->width();
+
+       if (!data_ || (data_size_ == 0))
+               return QSize(width, 0);
+
+       size_t height = data_size_ / BYTES_PER_LINE;
+
+       if (data_size_ % BYTES_PER_LINE)
+               height++;
+
+       height *= charHeight_;
+
+       return QSize(width, height);
+}
+
+void QHexView::paintEvent(QPaintEvent *event)
+{
+       QPainter painter(viewport());
+
+       // Calculate and update the widget and paint area sizes
+       QSize widgetSize = getFullSize();
+       setMinimumWidth(widgetSize.width());
+       setMaximumWidth(widgetSize.width());
+       QSize areaSize = viewport()->size() - QSize(0, charHeight_);
+
+       // Only show scrollbar if the content goes beyond the visible area
+       if (widgetSize.height() > areaSize.height()) {
+               verticalScrollBar()->setEnabled(true);
+               verticalScrollBar()->setPageStep(areaSize.height() / charHeight_);
+               verticalScrollBar()->setRange(0, ((widgetSize.height() - areaSize.height())) / charHeight_ + 1);
+       } else
+               verticalScrollBar()->setEnabled(false);
+
+       // Fill widget background
+       painter.fillRect(event->rect(), palette().color(QPalette::Base));
+
+       if (!data_ || (data_size_ == 0) || (data_->chunks.empty())) {
+               painter.setPen(palette().color(QPalette::Text));
+               QString s = tr("No data available");
+               int x = (areaSize.width() - fontMetrics().boundingRect(s).width()) / 2;
+               int y = areaSize.height() / 2;
+               painter.drawText(x, y, s);
+               return;
+       }
+
+       // Determine first/last line indices
+       size_t firstLineIdx = verticalScrollBar()->value();
+
+       size_t lastLineIdx = firstLineIdx + (areaSize.height() / charHeight_);
+       if (lastLineIdx > (data_size_ / BYTES_PER_LINE)) {
+               lastLineIdx = data_size_ / BYTES_PER_LINE;
+               if (data_size_ % BYTES_PER_LINE)
+                       lastLineIdx++;
+       }
+
+       // Paint divider line between hex and ASCII areas
+       int line_x = posAscii_ - (GAP_HEX_ASCII / 2);
+       painter.setPen(palette().color(QPalette::Midlight));
+       painter.drawLine(line_x, event->rect().top(), line_x, height());
+
+       // Fill address area background
+       painter.fillRect(QRect(posAddr_, event->rect().top(),
+               posHex_ - (GAP_ADR_HEX / 2), height()), palette().color(QPalette::Window));
+       painter.fillRect(QRect(posAddr_, event->rect().top(),
+               posAscii_ - (GAP_HEX_ASCII / 2), charHeight_ + 2), palette().color(QPalette::Window));
+
+       // Paint address area
+       painter.setPen(palette().color(QPalette::ButtonText));
+
+       int yStart = 2 * charHeight_;
+       for (size_t lineIdx = firstLineIdx, y = yStart; lineIdx < lastLineIdx; lineIdx++) {
+
+               QString address = QString("%1").arg(lineIdx * 16, 10, 16, QChar('0')).toUpper();
+               painter.drawText(posAddr_, y, address);
+               y += charHeight_;
+       }
+
+       // Paint top row with hex offsets
+       painter.setPen(palette().color(QPalette::ButtonText));
+       for (int offset = 0; offset <= 0xF; offset++)
+               painter.drawText(posHex_ + (1 + offset * 3) * charWidth_,
+                       charHeight_ - 3, QString::number(offset, 16).toUpper());
+
+       // Paint hex values
+       QBrush regular = palette().buttonText();
+       QBrush selected = palette().highlight();
+
+       bool multiple_chunks = (data_->chunks.size() > 1);
+       unsigned int chunk_color = 0;
+
+       initialize_byte_iterator(firstLineIdx * BYTES_PER_LINE);
+       yStart = 2 * charHeight_;
+       for (size_t lineIdx = firstLineIdx, y = yStart; lineIdx < lastLineIdx; lineIdx++) {
+
+               int x = posHex_;
+               for (size_t i = 0; (i < BYTES_PER_LINE) && (current_offset_ < data_size_); i++) {
+                       size_t pos = (lineIdx * BYTES_PER_LINE + i) * 2;
+
+                       // Fetch byte
+                       bool is_next_chunk;
+                       uint8_t byte_value = get_next_byte(&is_next_chunk);
+
+                       if (is_next_chunk) {
+                               chunk_color++;
+                               if (chunk_color == chunk_colors_.size())
+                                       chunk_color = 0;
+                       }
+
+                       if ((pos >= selectBegin_) && (pos < selectEnd_)) {
+                               painter.setBackgroundMode(Qt::OpaqueMode);
+                               painter.setBackground(selected);
+                               painter.setPen(palette().color(QPalette::HighlightedText));
+                       } else {
+                               painter.setBackground(regular);
+                               painter.setBackgroundMode(Qt::TransparentMode);
+                               if (!multiple_chunks)
+                                       painter.setPen(palette().color(QPalette::Text));
+                               else
+                                       painter.setPen(chunk_colors_[chunk_color]);
+                       }
+
+                       // First nibble
+                       QString val = QString::number((byte_value & 0xF0) >> 4, 16).toUpper();
+                       painter.drawText(x, y, val);
+
+                       // Second nibble
+                       val = QString::number((byte_value & 0xF), 16).toUpper();
+                       painter.drawText(x + charWidth_, y, val);
+
+                       if ((pos >= selectBegin_) && (pos < selectEnd_ - 1) && (i < BYTES_PER_LINE - 1))
+                               painter.drawText(x + 2 * charWidth_, y, QString(' '));
+
+                       x += 3 * charWidth_;
+               }
+
+               y += charHeight_;
+       }
+
+       // Paint ASCII characters
+       initialize_byte_iterator(firstLineIdx * BYTES_PER_LINE);
+       yStart = 2 * charHeight_;
+       for (size_t lineIdx = firstLineIdx, y = yStart; lineIdx < lastLineIdx; lineIdx++) {
+
+               int x = posAscii_;
+               for (size_t i = 0; (i < BYTES_PER_LINE) && (current_offset_ < data_size_); i++) {
+                       // Fetch byte
+                       uint8_t ch = get_next_byte();
+
+                       if ((ch < 0x20) || (ch > 0x7E))
+                               ch = '.';
+
+                       size_t pos = (lineIdx * BYTES_PER_LINE + i) * 2;
+                       if ((pos >= selectBegin_) && (pos < selectEnd_)) {
+                               painter.setBackgroundMode(Qt::OpaqueMode);
+                               painter.setBackground(selected);
+                               painter.setPen(palette().color(QPalette::HighlightedText));
+                       } else {
+                               painter.setBackgroundMode(Qt::TransparentMode);
+                               painter.setBackground(regular);
+                               painter.setPen(palette().color(QPalette::Text));
+                       }
+
+                       painter.drawText(x, y, QString(ch));
+                       x += charWidth_;
+               }
+
+               y += charHeight_;
+       }
+
+       // Paint cursor
+       if (hasFocus()) {
+               int x = (cursorPos_ % (2 * BYTES_PER_LINE));
+               int y = cursorPos_ / (2 * BYTES_PER_LINE);
+               y -= firstLineIdx;
+               int cursorX = (((x / 2) * 3) + (x % 2)) * charWidth_ + posHex_;
+               int cursorY = charHeight_ + y * charHeight_ + 4;
+               painter.fillRect(cursorX, cursorY, 2, charHeight_, palette().color(QPalette::WindowText));
+       }
+}
+
+void QHexView::keyPressEvent(QKeyEvent *event)
+{
+       bool setVisible = false;
+
+       // Cursor movements
+       if (event->matches(QKeySequence::MoveToNextChar)) {
+               setCursorPos(cursorPos_ + 1);
+               resetSelection(cursorPos_);
+               setVisible = true;
+       }
+       if (event->matches(QKeySequence::MoveToPreviousChar)) {
+               setCursorPos(cursorPos_ - 1);
+               resetSelection(cursorPos_);
+               setVisible = true;
+       }
+
+       if (event->matches(QKeySequence::MoveToEndOfLine)) {
+               setCursorPos(cursorPos_ | ((BYTES_PER_LINE * 2) - 1));
+               resetSelection(cursorPos_);
+               setVisible = true;
+       }
+       if (event->matches(QKeySequence::MoveToStartOfLine)) {
+               setCursorPos(cursorPos_ | (cursorPos_ % (BYTES_PER_LINE * 2)));
+               resetSelection(cursorPos_);
+               setVisible = true;
+       }
+       if (event->matches(QKeySequence::MoveToPreviousLine)) {
+               setCursorPos(cursorPos_ - BYTES_PER_LINE * 2);
+               resetSelection(cursorPos_);
+               setVisible = true;
+       }
+       if (event->matches(QKeySequence::MoveToNextLine)) {
+               setCursorPos(cursorPos_ + BYTES_PER_LINE * 2);
+               resetSelection(cursorPos_);
+               setVisible = true;
+       }
+
+       if (event->matches(QKeySequence::MoveToNextPage)) {
+               setCursorPos(cursorPos_ + (viewport()->height() / charHeight_ - 1) * 2 * BYTES_PER_LINE);
+               resetSelection(cursorPos_);
+               setVisible = true;
+       }
+       if (event->matches(QKeySequence::MoveToPreviousPage)) {
+               setCursorPos(cursorPos_ - (viewport()->height() / charHeight_ - 1) * 2 * BYTES_PER_LINE);
+               resetSelection(cursorPos_);
+               setVisible = true;
+       }
+       if (event->matches(QKeySequence::MoveToEndOfDocument)) {
+               setCursorPos(data_size_ * 2);
+               resetSelection(cursorPos_);
+               setVisible = true;
+       }
+       if (event->matches(QKeySequence::MoveToStartOfDocument)) {
+               setCursorPos(0);
+               resetSelection(cursorPos_);
+               setVisible = true;
+       }
+
+       // Select commands
+       if (event->matches(QKeySequence::SelectAll)) {
+               resetSelection(0);
+               setSelection(2 * data_size_);
+               setVisible = true;
+       }
+       if (event->matches(QKeySequence::SelectNextChar)) {
+               int pos = cursorPos_ + 1;
+               setCursorPos(pos);
+               setSelection(pos);
+               setVisible = true;
+       }
+       if (event->matches(QKeySequence::SelectPreviousChar)) {
+               int pos = cursorPos_ - 1;
+               setSelection(pos);
+               setCursorPos(pos);
+               setVisible = true;
+       }
+       if (event->matches(QKeySequence::SelectEndOfLine)) {
+               int pos = cursorPos_ - (cursorPos_ % (2 * BYTES_PER_LINE)) + (2 * BYTES_PER_LINE);
+               setCursorPos(pos);
+               setSelection(pos);
+               setVisible = true;
+       }
+       if (event->matches(QKeySequence::SelectStartOfLine)) {
+               int pos = cursorPos_ - (cursorPos_ % (2 * BYTES_PER_LINE));
+               setCursorPos(pos);
+               setSelection(pos);
+               setVisible = true;
+       }
+       if (event->matches(QKeySequence::SelectPreviousLine)) {
+               int pos = cursorPos_ - (2 * BYTES_PER_LINE);
+               setCursorPos(pos);
+               setSelection(pos);
+               setVisible = true;
+       }
+       if (event->matches(QKeySequence::SelectNextLine)) {
+               int pos = cursorPos_ + (2 * BYTES_PER_LINE);
+               setCursorPos(pos);
+               setSelection(pos);
+               setVisible = true;
+       }
+
+       if (event->matches(QKeySequence::SelectNextPage)) {
+               int pos = cursorPos_ + (((viewport()->height() / charHeight_) - 1) * 2 * BYTES_PER_LINE);
+               setCursorPos(pos);
+               setSelection(pos);
+               setVisible = true;
+       }
+       if (event->matches(QKeySequence::SelectPreviousPage)) {
+               int pos = cursorPos_ - (((viewport()->height() / charHeight_) - 1) * 2 * BYTES_PER_LINE);
+               setCursorPos(pos);
+               setSelection(pos);
+               setVisible = true;
+       }
+       if (event->matches(QKeySequence::SelectEndOfDocument)) {
+               int pos = data_size_ * 2;
+               setCursorPos(pos);
+               setSelection(pos);
+               setVisible = true;
+       }
+       if (event->matches(QKeySequence::SelectStartOfDocument)) {
+               setCursorPos(0);
+               setSelection(0);
+               setVisible = true;
+       }
+
+       if (event->matches(QKeySequence::Copy) && (data_)) {
+               QString text;
+
+               initialize_byte_iterator(selectBegin_ / 2);
+
+               size_t selectedSize = (selectEnd_ - selectBegin_ + 1) / 2;
+               for (size_t i = 0; i < selectedSize; i++) {
+                       uint8_t byte_value = get_next_byte();
+
+                       QString s = QString::number((byte_value & 0xF0) >> 4, 16).toUpper() +
+                               QString::number((byte_value & 0xF), 16).toUpper() + " ";
+                       text += s;
+
+                       if (i % BYTES_PER_LINE == (BYTES_PER_LINE - 1))
+                               text += "\n";
+               }
+
+               QClipboard *clipboard = QApplication::clipboard();
+               clipboard->setText(text, QClipboard::Clipboard);
+               if (clipboard->supportsSelection())
+                       clipboard->setText(text, QClipboard::Selection);
+       }
+
+       if (setVisible)
+               ensureVisible();
+
+       viewport()->update();
+}
+
+void QHexView::mouseMoveEvent(QMouseEvent *event)
+{
+       int actPos = cursorPosFromMousePos(event->pos());
+       setCursorPos(actPos);
+       setSelection(actPos);
+
+       viewport()->update();
+}
+
+void QHexView::mousePressEvent(QMouseEvent *event)
+{
+       int cPos = cursorPosFromMousePos(event->pos());
+
+       if ((QApplication::keyboardModifiers() & Qt::ShiftModifier) && (event->button() == Qt::LeftButton))
+               setSelection(cPos);
+       else
+               resetSelection(cPos);
+
+       setCursorPos(cPos);
+
+       viewport()->update();
+}
+
+size_t QHexView::cursorPosFromMousePos(const QPoint &position)
+{
+       size_t pos = -1;
+
+       if (((size_t)position.x() >= posHex_) &&
+               ((size_t)position.x() < (posHex_ + HEXCHARS_IN_LINE * charWidth_))) {
+
+               // Note: We add 1.5 character widths so that selection across
+               // byte gaps is smoother
+               size_t x = (position.x() + (1.5 * charWidth_ / 2) - posHex_) / charWidth_;
+
+               // Note: We allow only full bytes to be selected, not nibbles,
+               // so we round to the nearest byte gap
+               x = (2 * x + 1) / 3;
+
+               size_t firstLineIdx = verticalScrollBar()->value();
+               size_t y = ((position.y() / charHeight_) - 1) * 2 * BYTES_PER_LINE;
+               pos = x + y + firstLineIdx * BYTES_PER_LINE * 2;
+       }
+
+       size_t max_pos = data_size_ * 2;
+
+       return std::min(pos, max_pos);
+}
+
+void QHexView::resetSelection()
+{
+       selectBegin_ = selectInit_;
+       selectEnd_ = selectInit_;
+}
+
+void QHexView::resetSelection(int pos)
+{
+       if (pos < 0)
+               pos = 0;
+
+       selectInit_ = pos;
+       selectBegin_ = pos;
+       selectEnd_ = pos;
+}
+
+void QHexView::setSelection(int pos)
+{
+       if (pos < 0)
+               pos = 0;
+
+       if ((size_t)pos >= selectInit_) {
+               selectEnd_ = pos;
+               selectBegin_ = selectInit_;
+       } else {
+               selectBegin_ = pos;
+               selectEnd_ = selectInit_;
+       }
+}
+
+void QHexView::setCursorPos(int position)
+{
+       if (position < 0)
+               position = 0;
+
+       int max_pos = data_size_ * 2;
+
+       if (position > max_pos)
+               position = max_pos;
+
+       cursorPos_ = position;
+}
+
+void QHexView::ensureVisible()
+{
+       QSize areaSize = viewport()->size();
+
+       int firstLineIdx = verticalScrollBar()->value();
+       int lastLineIdx = firstLineIdx + areaSize.height() / charHeight_;
+
+       int cursorY = cursorPos_ / (2 * BYTES_PER_LINE);
+
+       if (cursorY < firstLineIdx)
+               verticalScrollBar()->setValue(cursorY);
+       else
+               if(cursorY >= lastLineIdx)
+                       verticalScrollBar()->setValue(cursorY - areaSize.height() / charHeight_ + 1);
+}
diff --git a/pv/views/decoder_binary/QHexView.hpp b/pv/views/decoder_binary/QHexView.hpp
new file mode 100644 (file)
index 0000000..c39dcb2
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 Victor Anjin <virinext@gmail.com>
+ * Copyright (C) 2019 Soeren Apel <soeren@apelpie.net>
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#ifndef PULSEVIEW_PV_VIEWS_DECODERBINARY_QHEXVIEW_H
+#define PULSEVIEW_PV_VIEWS_DECODERBINARY_QHEXVIEW_H
+
+#include <QAbstractScrollArea>
+
+#include <pv/data/decodesignal.hpp>
+
+using std::pair;
+using std::size_t;
+using pv::data::DecodeBinaryClass;
+using pv::data::DecodeBinaryDataChunk;
+
+class QHexView: public QAbstractScrollArea
+{
+       Q_OBJECT
+
+public:
+       enum Mode {
+               ChunkedDataMode,    ///< Displays all data chunks in succession
+               MemoryEmulationMode ///< Reconstructs memory contents from data chunks
+       };
+
+public:
+       QHexView(QWidget *parent = nullptr);
+
+       void set_mode(Mode m);
+       void set_data(const DecodeBinaryClass* data);
+       unsigned int get_bytes_per_line() const;
+
+       void clear();
+       void showFromOffset(size_t offset);
+       virtual QSizePolicy sizePolicy() const;
+
+       pair<size_t, size_t> get_selection() const;
+
+       size_t create_hex_line(size_t start, size_t end, QString* dest,
+               bool with_offset=false, bool with_ascii=false);
+
+protected:
+       void initialize_byte_iterator(size_t offset);
+       uint8_t get_next_byte(bool* is_next_chunk = nullptr);
+
+       void paintEvent(QPaintEvent *event);
+       void keyPressEvent(QKeyEvent *event);
+       void mouseMoveEvent(QMouseEvent *event);
+       void mousePressEvent(QMouseEvent *event);
+
+private:
+       QSize getFullSize() const;
+       void resetSelection();
+       void resetSelection(int pos);
+       void setSelection(int pos);
+       void ensureVisible();
+       void setCursorPos(int pos);
+       size_t cursorPosFromMousePos(const QPoint &position);
+
+private:
+       Mode mode_;
+       const DecodeBinaryClass* data_;
+       size_t data_size_;
+
+       size_t posAddr_, posHex_, posAscii_;
+       size_t charWidth_, charHeight_;
+       size_t selectBegin_, selectEnd_, selectInit_, cursorPos_;
+
+       size_t current_chunk_id_, current_chunk_offset_, current_offset_;
+       DecodeBinaryDataChunk current_chunk_; // Cache locally so that we're not messed up when the vector is re-allocating its data
+
+       vector<QColor> chunk_colors_;
+};
+
+#endif /* PULSEVIEW_PV_VIEWS_DECODERBINARY_QHEXVIEW_H */
diff --git a/pv/views/decoder_binary/view.cpp b/pv/views/decoder_binary/view.cpp
new file mode 100644 (file)
index 0000000..5f55b19
--- /dev/null
@@ -0,0 +1,480 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2019 Soeren Apel <soeren@apelpie.net>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <climits>
+
+#include <QByteArray>
+#include <QDebug>
+#include <QFileDialog>
+#include <QLabel>
+#include <QMenu>
+#include <QMessageBox>
+#include <QToolBar>
+#include <QVBoxLayout>
+
+#include <libsigrokdecode/libsigrokdecode.h>
+
+#include "view.hpp"
+#include "QHexView.hpp"
+
+#include "pv/globalsettings.hpp"
+#include "pv/session.hpp"
+#include "pv/util.hpp"
+#include "pv/data/decode/decoder.hpp"
+
+using pv::data::DecodeSignal;
+using pv::data::SignalBase;
+using pv::data::decode::Decoder;
+using pv::util::Timestamp;
+
+using std::shared_ptr;
+
+namespace pv {
+namespace views {
+namespace decoder_binary {
+
+const char* SaveTypeNames[SaveTypeCount] = {
+       "Binary",
+       "Hex Dump, plain",
+       "Hex Dump, with offset",
+       "Hex Dump, canonical"
+};
+
+
+View::View(Session &session, bool is_main_view, QMainWindow *parent) :
+       ViewBase(session, is_main_view, parent),
+
+       // Note: Place defaults in View::reset_view_state(), not here
+       parent_(parent),
+       decoder_selector_(new QComboBox()),
+       format_selector_(new QComboBox()),
+       class_selector_(new QComboBox()),
+       stacked_widget_(new QStackedWidget()),
+       hex_view_(new QHexView()),
+       save_button_(new QToolButton()),
+       save_action_(new QAction(this)),
+       signal_(nullptr)
+{
+       QVBoxLayout *root_layout = new QVBoxLayout(this);
+       root_layout->setContentsMargins(0, 0, 0, 0);
+
+       // Create toolbar
+       QToolBar* toolbar = new QToolBar();
+       toolbar->setContextMenuPolicy(Qt::PreventContextMenu);
+       parent->addToolBar(toolbar);
+
+       // Populate toolbar
+       toolbar->addWidget(new QLabel(tr("Decoder:")));
+       toolbar->addWidget(decoder_selector_);
+       toolbar->addWidget(class_selector_);
+       toolbar->addSeparator();
+       toolbar->addWidget(new QLabel(tr("Show data as")));
+       toolbar->addWidget(format_selector_);
+       toolbar->addSeparator();
+       toolbar->addWidget(save_button_);
+
+       // Add format types
+       format_selector_->addItem(tr("Hexdump"), qVariantFromValue(QString("text/hexdump")));
+
+       // Add widget stack
+       root_layout->addWidget(stacked_widget_);
+       stacked_widget_->addWidget(hex_view_);
+       stacked_widget_->setCurrentIndex(0);
+
+       connect(decoder_selector_, SIGNAL(currentIndexChanged(int)),
+               this, SLOT(on_selected_decoder_changed(int)));
+       connect(class_selector_, SIGNAL(currentIndexChanged(int)),
+               this, SLOT(on_selected_class_changed(int)));
+
+       // Configure widgets
+       decoder_selector_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
+       class_selector_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
+
+       // Configure actions
+       save_action_->setText(tr("&Save..."));
+       save_action_->setIcon(QIcon::fromTheme("document-save-as",
+               QIcon(":/icons/document-save-as.png")));
+       save_action_->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_S));
+       connect(save_action_, SIGNAL(triggered(bool)),
+               this, SLOT(on_actionSave_triggered()));
+
+       QMenu *save_menu = new QMenu();
+       connect(save_menu, SIGNAL(triggered(QAction*)),
+               this, SLOT(on_actionSave_triggered(QAction*)));
+
+       for (int i = 0; i < SaveTypeCount; i++) {
+               QAction *const action = save_menu->addAction(tr(SaveTypeNames[i]));
+               action->setData(qVariantFromValue(i));
+       }
+
+       save_button_->setMenu(save_menu);
+       save_button_->setDefaultAction(save_action_);
+       save_button_->setPopupMode(QToolButton::MenuButtonPopup);
+
+       parent->setSizePolicy(hex_view_->sizePolicy()); // TODO Must be updated when selected widget changes
+
+       reset_view_state();
+}
+
+ViewType View::get_type() const
+{
+       return ViewTypeDecoderBinary;
+}
+
+void View::reset_view_state()
+{
+       ViewBase::reset_view_state();
+
+       decoder_selector_->clear();
+       class_selector_->clear();
+       format_selector_->setCurrentIndex(0);
+       save_button_->setEnabled(false);
+
+       hex_view_->clear();
+}
+
+void View::clear_decode_signals()
+{
+       ViewBase::clear_decode_signals();
+
+       reset_data();
+       reset_view_state();
+}
+
+void View::add_decode_signal(shared_ptr<data::DecodeSignal> signal)
+{
+       ViewBase::add_decode_signal(signal);
+
+       connect(signal.get(), SIGNAL(name_changed(const QString&)),
+               this, SLOT(on_signal_name_changed(const QString&)));
+       connect(signal.get(), SIGNAL(decoder_stacked(void*)),
+               this, SLOT(on_decoder_stacked(void*)));
+       connect(signal.get(), SIGNAL(decoder_removed(void*)),
+               this, SLOT(on_decoder_removed(void*)));
+
+       // Add all decoders provided by this signal
+       auto stack = signal->decoder_stack();
+       if (stack.size() > 1) {
+               for (const shared_ptr<Decoder>& dec : stack)
+                       // Only add the decoder if it has binary output
+                       if (dec->get_binary_class_count() > 0) {
+                               QString title = QString("%1 (%2)").arg(signal->name(), dec->name());
+                               decoder_selector_->addItem(title, QVariant::fromValue((void*)dec.get()));
+                       }
+       } else
+               if (!stack.empty()) {
+                       shared_ptr<Decoder>& dec = stack.at(0);
+                       if (dec->get_binary_class_count() > 0)
+                               decoder_selector_->addItem(signal->name(), QVariant::fromValue((void*)dec.get()));
+               }
+}
+
+void View::remove_decode_signal(shared_ptr<data::DecodeSignal> signal)
+{
+       // Remove all decoders provided by this signal
+       for (const shared_ptr<Decoder>& dec : signal->decoder_stack()) {
+               int index = decoder_selector_->findData(QVariant::fromValue((void*)dec.get()));
+
+               if (index != -1)
+                       decoder_selector_->removeItem(index);
+       }
+
+       ViewBase::remove_decode_signal(signal);
+
+       if (signal.get() == signal_) {
+               reset_data();
+               update_data();
+               reset_view_state();
+       }
+}
+
+void View::save_settings(QSettings &settings) const
+{
+       (void)settings;
+}
+
+void View::restore_settings(QSettings &settings)
+{
+       // Note: It is assumed that this function is only called once,
+       // immediately after restoring a previous session.
+       (void)settings;
+}
+
+void View::reset_data()
+{
+       signal_ = nullptr;
+       decoder_ = nullptr;
+       bin_class_id_ = 0;
+       binary_data_exists_ = false;
+
+       hex_view_->clear();
+}
+
+void View::update_data()
+{
+       if (!signal_)
+               return;
+
+       const DecodeBinaryClass* bin_class =
+               signal_->get_binary_data_class(current_segment_, decoder_, bin_class_id_);
+
+       hex_view_->set_data(bin_class);
+
+       if (!binary_data_exists_)
+               return;
+
+       if (!save_button_->isEnabled())
+               save_button_->setEnabled(true);
+}
+
+void View::save_data() const
+{
+       assert(decoder_);
+       assert(signal_);
+
+       if (!signal_)
+               return;
+
+       GlobalSettings settings;
+       const QString dir = settings.value("MainWindow/SaveDirectory").toString();
+
+       const QString file_name = QFileDialog::getSaveFileName(
+               parent_, tr("Save Binary Data"), dir, tr("Binary Data Files (*.bin);;All Files (*)"));
+
+       if (file_name.isEmpty())
+               return;
+
+       QFile file(file_name);
+       if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+               pair<size_t, size_t> selection = hex_view_->get_selection();
+
+               vector<uint8_t> data;
+               data.resize(selection.second - selection.first + 1);
+
+               signal_->get_merged_binary_data_chunks_by_offset(current_segment_, decoder_,
+                       bin_class_id_, selection.first, selection.second, &data);
+
+               int64_t bytes_written = file.write((const char*)data.data(), data.size());
+
+               if ((bytes_written == -1) || ((uint64_t)bytes_written != data.size())) {
+                       QMessageBox msg(parent_);
+                       msg.setText(tr("Error") + "\n\n" + tr("File %1 could not be written to.").arg(file_name));
+                       msg.setStandardButtons(QMessageBox::Ok);
+                       msg.setIcon(QMessageBox::Warning);
+                       msg.exec();
+                       return;
+               }
+       }
+}
+
+void View::save_data_as_hex_dump(bool with_offset, bool with_ascii) const
+{
+       assert(decoder_);
+       assert(signal_);
+
+       if (!signal_)
+               return;
+
+       GlobalSettings settings;
+       const QString dir = settings.value("MainWindow/SaveDirectory").toString();
+
+       const QString file_name = QFileDialog::getSaveFileName(
+               parent_, tr("Save Binary Data"), dir, tr("Hex Dumps (*.txt);;All Files (*)"));
+
+       if (file_name.isEmpty())
+               return;
+
+       QFile file(file_name);
+       if (file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
+               pair<size_t, size_t> selection = hex_view_->get_selection();
+
+               vector<uint8_t> data;
+               data.resize(selection.second - selection.first + 1);
+
+               signal_->get_merged_binary_data_chunks_by_offset(current_segment_, decoder_,
+                       bin_class_id_, selection.first, selection.second, &data);
+
+               QTextStream out_stream(&file);
+
+               uint64_t offset = selection.first;
+               uint64_t n = hex_view_->get_bytes_per_line();
+               QString s;
+
+               while (offset < selection.second) {
+                       size_t end = std::min((uint64_t)(selection.second), offset + n);
+                       offset = hex_view_->create_hex_line(offset, end, &s, with_offset, with_ascii);
+                       out_stream << s << endl;
+               }
+
+               out_stream << endl;
+
+               if (out_stream.status() != QTextStream::Ok) {
+                       QMessageBox msg(parent_);
+                       msg.setText(tr("Error") + "\n\n" + tr("File %1 could not be written to.").arg(file_name));
+                       msg.setStandardButtons(QMessageBox::Ok);
+                       msg.setIcon(QMessageBox::Warning);
+                       msg.exec();
+                       return;
+               }
+       }
+}
+
+void View::on_selected_decoder_changed(int index)
+{
+       if (signal_)
+               disconnect(signal_, SIGNAL(new_binary_data(unsigned int, void*, unsigned int)));
+
+       reset_data();
+
+       decoder_ = (Decoder*)decoder_selector_->itemData(index).value<void*>();
+
+       // Find the signal that contains the selected decoder
+       for (const shared_ptr<DecodeSignal>& ds : decode_signals_)
+               for (const shared_ptr<Decoder>& dec : ds->decoder_stack())
+                       if (decoder_ == dec.get())
+                               signal_ = ds.get();
+
+       class_selector_->clear();
+
+       if (signal_) {
+               // Populate binary class selector
+               uint32_t bin_classes = decoder_->get_binary_class_count();
+               for (uint32_t i = 0; i < bin_classes; i++) {
+                       const data::decode::DecodeBinaryClassInfo* class_info = decoder_->get_binary_class(i);
+                       class_selector_->addItem(class_info->description, QVariant::fromValue(i));
+               }
+
+               connect(signal_, SIGNAL(new_binary_data(unsigned int, void*, unsigned int)),
+                       this, SLOT(on_new_binary_data(unsigned int, void*, unsigned int)));
+       }
+
+       update_data();
+}
+
+void View::on_selected_class_changed(int index)
+{
+       bin_class_id_ = class_selector_->itemData(index).value<uint32_t>();
+
+       binary_data_exists_ =
+               signal_->get_binary_data_chunk_count(current_segment_, decoder_, bin_class_id_);
+
+       update_data();
+}
+
+void View::on_signal_name_changed(const QString &name)
+{
+       (void)name;
+
+       SignalBase* sb = qobject_cast<SignalBase*>(QObject::sender());
+       assert(sb);
+
+       DecodeSignal* signal = dynamic_cast<DecodeSignal*>(sb);
+       assert(signal);
+
+       // Update all decoder entries provided by this signal
+       auto stack = signal->decoder_stack();
+       if (stack.size() > 1) {
+               for (const shared_ptr<Decoder>& dec : stack) {
+                       QString title = QString("%1 (%2)").arg(signal->name(), dec->name());
+                       int index = decoder_selector_->findData(QVariant::fromValue((void*)dec.get()));
+
+                       if (index != -1)
+                               decoder_selector_->setItemText(index, title);
+               }
+       } else
+               if (!stack.empty()) {
+                       shared_ptr<Decoder>& dec = stack.at(0);
+                       int index = decoder_selector_->findData(QVariant::fromValue((void*)dec.get()));
+
+                       if (index != -1)
+                               decoder_selector_->setItemText(index, signal->name());
+               }
+}
+
+void View::on_new_binary_data(unsigned int segment_id, void* decoder, unsigned int bin_class_id)
+{
+       if ((segment_id == current_segment_) && (decoder == decoder_) && (bin_class_id == bin_class_id_))
+               if (!delayed_view_updater_.isActive())
+                       delayed_view_updater_.start();
+}
+
+void View::on_decoder_stacked(void* decoder)
+{
+       // TODO This doesn't change existing entries for the same signal - but it should as the naming scheme may change
+
+       Decoder* d = static_cast<Decoder*>(decoder);
+
+       // Only add the decoder if it has binary output
+       if (d->get_binary_class_count() == 0)
+               return;
+
+       // Find the signal that contains the selected decoder
+       DecodeSignal* signal = nullptr;
+
+       for (const shared_ptr<DecodeSignal>& ds : decode_signals_)
+               for (const shared_ptr<Decoder>& dec : ds->decoder_stack())
+                       if (d == dec.get())
+                               signal = ds.get();
+
+       assert(signal);
+
+       // Add the decoder to the list
+       QString title = QString("%1 (%2)").arg(signal->name(), d->name());
+       decoder_selector_->addItem(title, QVariant::fromValue((void*)d));
+}
+
+void View::on_decoder_removed(void* decoder)
+{
+       Decoder* d = static_cast<Decoder*>(decoder);
+
+       // Remove the decoder from the list
+       int index = decoder_selector_->findData(QVariant::fromValue((void*)d));
+
+       if (index != -1)
+               decoder_selector_->removeItem(index);
+}
+
+void View::on_actionSave_triggered(QAction* action)
+{
+       int save_type = SaveTypeBinary;
+       if (action)
+               save_type = action->data().toInt();
+
+       switch (save_type)
+       {
+       case SaveTypeBinary: save_data(); break;
+       case SaveTypeHexDumpPlain: save_data_as_hex_dump(false, false); break;
+       case SaveTypeHexDumpWithOffset: save_data_as_hex_dump(true, false); break;
+       case SaveTypeHexDumpComplete: save_data_as_hex_dump(true, true); break;
+       }
+}
+
+void View::perform_delayed_view_update()
+{
+       if (signal_ && !binary_data_exists_)
+               if (signal_->get_binary_data_chunk_count(current_segment_, decoder_, bin_class_id_))
+                       binary_data_exists_ = true;
+
+       update_data();
+}
+
+
+} // namespace decoder_binary
+} // namespace views
+} // namespace pv
diff --git a/pv/views/decoder_binary/view.hpp b/pv/views/decoder_binary/view.hpp
new file mode 100644 (file)
index 0000000..c1d70b1
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2019 Soeren Apel <soeren@apelpie.net>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef PULSEVIEW_PV_VIEWS_DECODERBINARY_VIEW_HPP
+#define PULSEVIEW_PV_VIEWS_DECODERBINARY_VIEW_HPP
+
+#include <QAction>
+#include <QComboBox>
+#include <QStackedWidget>
+#include <QToolButton>
+
+#include <pv/views/viewbase.hpp>
+#include <pv/data/decodesignal.hpp>
+
+#include "QHexView.hpp"
+
+namespace pv {
+
+class Session;
+
+namespace views {
+
+namespace decoder_binary {
+
+// When adding an entry here, don't forget to update SaveTypeNames as well
+enum SaveType {
+       SaveTypeBinary,
+       SaveTypeHexDumpPlain,
+       SaveTypeHexDumpWithOffset,
+       SaveTypeHexDumpComplete,
+       SaveTypeCount  // Indicates how many save types there are, must always be last
+};
+
+extern const char* SaveTypeNames[SaveTypeCount];
+
+
+class View : public ViewBase
+{
+       Q_OBJECT
+
+public:
+       explicit View(Session &session, bool is_main_view=false, QMainWindow *parent = nullptr);
+
+       virtual ViewType get_type() const;
+
+       /**
+        * Resets the view to its default state after construction. It does however
+        * not reset the signal bases or any other connections with the session.
+        */
+       virtual void reset_view_state();
+
+       virtual void clear_decode_signals();
+       virtual void add_decode_signal(shared_ptr<data::DecodeSignal> signal);
+       virtual void remove_decode_signal(shared_ptr<data::DecodeSignal> signal);
+
+       virtual void save_settings(QSettings &settings) const;
+       virtual void restore_settings(QSettings &settings);
+
+private:
+       void reset_data();
+       void update_data();
+
+       void save_data() const;
+       void save_data_as_hex_dump(bool with_offset=false, bool with_ascii=false) const;
+
+private Q_SLOTS:
+       void on_selected_decoder_changed(int index);
+       void on_selected_class_changed(int index);
+       void on_signal_name_changed(const QString &name);
+       void on_new_binary_data(unsigned int segment_id, void* decoder, unsigned int bin_class_id);
+
+       void on_decoder_stacked(void* decoder);
+       void on_decoder_removed(void* decoder);
+
+       void on_actionSave_triggered(QAction* action = nullptr);
+
+       virtual void perform_delayed_view_update();
+
+private:
+       QWidget* parent_;
+
+       QComboBox *decoder_selector_, *format_selector_, *class_selector_;
+       QStackedWidget *stacked_widget_;
+       QHexView *hex_view_;
+
+       QToolButton* save_button_;
+       QAction* save_action_;
+
+       data::DecodeSignal *signal_;
+       const data::decode::Decoder *decoder_;
+       uint32_t bin_class_id_;
+       bool binary_data_exists_;
+};
+
+} // namespace decoder_binary
+} // namespace views
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEWS_DECODERBINARY_VIEW_HPP
index cd680085312b07d5249fecca2f51b0c32cdb39de..a0dd9eb239cded2d3e279cc01b78c8a1476b500a 100644 (file)
@@ -95,8 +95,8 @@ const int64_t AnalogSignal::TracePaintBlockSize = 1024 * 1024;  // 4 MiB (due to
 const float AnalogSignal::EnvelopeThreshold = 64.0f;
 
 const int AnalogSignal::MaximumVDivs = 10;
-const int AnalogSignal::MinScaleIndex = -6;
-const int AnalogSignal::MaxScaleIndex = 7;
+const int AnalogSignal::MinScaleIndex = -6;  // 0.01 units/div
+const int AnalogSignal::MaxScaleIndex = 10;  // 1000 units/div
 
 const int AnalogSignal::InfoTextMarginRight = 20;
 const int AnalogSignal::InfoTextMarginBottom = 5;
@@ -105,13 +105,13 @@ AnalogSignal::AnalogSignal(
        pv::Session &session,
        shared_ptr<data::SignalBase> base) :
        Signal(session, base),
+       value_at_hover_pos_(std::numeric_limits<float>::quiet_NaN()),
        scale_index_(4), // 20 per div
        pos_vdivs_(1),
        neg_vdivs_(1),
        resolution_(0),
        display_type_(DisplayBoth),
-       autoranging_(true),
-       value_at_hover_pos_(std::numeric_limits<float>::quiet_NaN())
+       autoranging_(true)
 {
        axis_pen_ = AxisPen;
 
@@ -143,38 +143,48 @@ shared_ptr<pv::data::SignalData> AnalogSignal::data() const
        return base_->analog_data();
 }
 
-void AnalogSignal::save_settings(QSettings &settings) const
+std::map<QString, QVariant> AnalogSignal::save_settings() const
 {
-       settings.setValue("pos_vdivs", pos_vdivs_);
-       settings.setValue("neg_vdivs", neg_vdivs_);
-       settings.setValue("scale_index", scale_index_);
-       settings.setValue("display_type", display_type_);
-       settings.setValue("autoranging", autoranging_);
-       settings.setValue("div_height", div_height_);
+       std::map<QString, QVariant> result;
+
+       result["pos_vdivs"] = pos_vdivs_;
+       result["neg_vdivs"] = neg_vdivs_;
+       result["scale_index"] = scale_index_;
+       result["display_type"] = display_type_;
+       result["autoranging"] = pos_vdivs_;
+       result["div_height"] = div_height_;
+
+       return result;
 }
 
-void AnalogSignal::restore_settings(QSettings &settings)
+void AnalogSignal::restore_settings(std::map<QString, QVariant> settings)
 {
-       if (settings.contains("pos_vdivs"))
-               pos_vdivs_ = settings.value("pos_vdivs").toInt();
+       auto entry = settings.find("pos_vdivs");
+       if (entry != settings.end())
+               pos_vdivs_ = settings["pos_vdivs"].toInt();
 
-       if (settings.contains("neg_vdivs"))
-               neg_vdivs_ = settings.value("neg_vdivs").toInt();
+       entry = settings.find("neg_vdivs");
+       if (entry != settings.end())
+               neg_vdivs_ = settings["neg_vdivs"].toInt();
 
-       if (settings.contains("scale_index")) {
-               scale_index_ = settings.value("scale_index").toInt();
+       entry = settings.find("scale_index");
+       if (entry != settings.end()) {
+               scale_index_ = settings["scale_index"].toInt();
                update_scale();
        }
 
-       if (settings.contains("display_type"))
-               display_type_ = (DisplayType)(settings.value("display_type").toInt());
+       entry = settings.find("display_type");
+       if (entry != settings.end())
+               display_type_ = (DisplayType)(settings["display_type"].toInt());
 
-       if (settings.contains("autoranging"))
-               autoranging_ = settings.value("autoranging").toBool();
+       entry = settings.find("autoranging");
+       if (entry != settings.end())
+               autoranging_ = settings["autoranging"].toBool();
 
-       if (settings.contains("div_height")) {
+       entry = settings.find("div_height");
+       if (entry != settings.end()) {
                const int old_height = div_height_;
-               div_height_ = settings.value("div_height").toInt();
+               div_height_ = settings["div_height"].toInt();
 
                if ((div_height_ != old_height) && owner_) {
                        // Call order is important, otherwise the lazy event handler won't work
@@ -297,7 +307,7 @@ void AnalogSignal::paint_fore(QPainter &p, ViewItemPaintParams &pp)
                // and we have corresponding data available
                if (show_hover_marker_ && !std::isnan(value_at_hover_pos_)) {
                        infotext = QString("[%1] %2 V/div")
-                               .arg(format_value_si(value_at_hover_pos_, SIPrefix::unspecified, 0, "V", false))
+                               .arg(format_value_si(value_at_hover_pos_, SIPrefix::unspecified, 2, "V", false))
                                .arg(resolution_);
                } else
                        infotext = QString("%1 V/div").arg(resolution_);
@@ -319,6 +329,7 @@ void AnalogSignal::paint_fore(QPainter &p, ViewItemPaintParams &pp)
 
 void AnalogSignal::paint_grid(QPainter &p, int y, int left, int right)
 {
+       bool was_antialiased = p.testRenderHint(QPainter::Antialiasing);
        p.setRenderHint(QPainter::Antialiasing, false);
 
        if (pos_vdivs_ > 0) {
@@ -363,7 +374,7 @@ void AnalogSignal::paint_grid(QPainter &p, int y, int left, int right)
                }
        }
 
-       p.setRenderHint(QPainter::Antialiasing, true);
+       p.setRenderHint(QPainter::Antialiasing, was_antialiased);
 }
 
 void AnalogSignal::paint_trace(QPainter &p,
@@ -443,7 +454,9 @@ void AnalogSignal::paint_trace(QPainter &p,
        }
        delete[] sample_block;
 
-       p.drawPolyline(points, points_count);
+       // QPainter::drawPolyline() is slow, let's paint the lines ourselves
+       for (int64_t i = 1; i < points_count; i++)
+               p.drawLine(points[i - 1], points[i]);
 
        if (show_sampling_points) {
                if (paint_thr_dots) {
@@ -839,7 +852,7 @@ void AnalogSignal::perform_autoranging(bool keep_divs, bool force_update)
        if (segments.empty())
                return;
 
-       static double prev_min = 0, prev_max = 0;
+       double signal_min_ = 0, signal_max_ = 0;
        double min = 0, max = 0;
 
        for (const shared_ptr<pv::data::AnalogSegment>& segment : segments) {
@@ -848,11 +861,11 @@ void AnalogSignal::perform_autoranging(bool keep_divs, bool force_update)
                max = std::max(max, mm.second);
        }
 
-       if ((min == prev_min) && (max == prev_max) && !force_update)
+       if ((min == signal_min_) && (max == signal_max_) && !force_update)
                return;
 
-       prev_min = min;
-       prev_max = max;
+       signal_min_ = min;
+       signal_max_ = max;
 
        // If we're allowed to alter the div assignment...
        if (!keep_divs) {
@@ -1080,11 +1093,10 @@ void AnalogSignal::hover_point_changed(const QPoint &hp)
        if (hp.x() <= 0) {
                value_at_hover_pos_ = std::numeric_limits<float>::quiet_NaN();
        } else {
-               try {
+               if ((size_t)hp.x() < value_at_pixel_pos_.size())
                        value_at_hover_pos_ = value_at_pixel_pos_.at(hp.x());
-               } catch (out_of_range&) {
+               else
                        value_at_hover_pos_ = std::numeric_limits<float>::quiet_NaN();
-               }
        }
 }
 
index fd724492c80e306dcabdf164fa3cd9b11f4789c2..c588eb88bf26e33abb7e7a8ac0fc53e7dc30af47 100644 (file)
@@ -77,9 +77,8 @@ public:
 
        shared_ptr<pv::data::SignalData> data() const;
 
-       virtual void save_settings(QSettings &settings) const;
-
-       virtual void restore_settings(QSettings &settings);
+       virtual std::map<QString, QVariant> save_settings() const;
+       virtual void restore_settings(std::map<QString, QVariant> settings);
 
        /**
         * Computes the vertical extents of the contents of this row item.
@@ -183,19 +182,12 @@ private:
                *display_type_cb_;
        QSpinBox *pvdiv_sb_, *nvdiv_sb_, *div_height_sb_;
 
-       float scale_;
-       int scale_index_;
-
-       int div_height_;
-       int pos_vdivs_, neg_vdivs_;  // divs per positive/negative side
-       float resolution_;  // e.g. 10 for 10 V/div
+       double signal_min_, signal_max_;  // Min/max values of this signal's analog data
 
        bool show_analog_minor_grid_;
        QColor high_fill_color_;
        bool show_sampling_points_, fill_high_areas_;
 
-       DisplayType display_type_;
-       bool autoranging_;
        int conversion_threshold_disp_mode_;
 
        vector<float> value_at_pixel_pos_;
@@ -203,6 +195,18 @@ private:
        float prev_value_at_pixel_;  // Only used during lookup table update
        float min_value_at_pixel_, max_value_at_pixel_;  // Only used during lookup table update
        int current_pixel_pos_;  // Only used during lookup table update
+
+       // ---------------------------------------------------------------------------
+       // Note: Make sure to update .. when adding a trace-configurable variable here
+       float scale_;
+       int scale_index_;
+
+       int div_height_;
+       int pos_vdivs_, neg_vdivs_;  // divs per positive/negative side
+       float resolution_;  // e.g. 10 for 10 V/div
+
+       DisplayType display_type_;
+       bool autoranging_;
 };
 
 } // namespace trace
index 7a375c7aad49410450c5f41f0d6d8c949e6bcd38..80eaba232b1d8157b2bbfacbb40ebe202e79670a 100644 (file)
@@ -25,6 +25,7 @@
 
 #include <QApplication>
 #include <QBrush>
+#include <QMenu>
 #include <QPainter>
 #include <QPointF>
 #include <QRect>
@@ -59,7 +60,8 @@ QString Cursor::get_text() const
        const pv::util::Timestamp& diff = abs(time_ - other->time_);
 
        return Ruler::format_time_with_distance(
-               diff, time_, view_.tick_prefix(), view_.time_unit(), view_.tick_precision());
+               diff, view_.ruler()->get_ruler_time_from_absolute_time(time_),
+        view_.tick_prefix(), view_.time_unit(), view_.tick_precision());
 }
 
 QRectF Cursor::label_rect(const QRectF &rect) const
@@ -88,6 +90,19 @@ QRectF Cursor::label_rect(const QRectF &rect) const
                return QRectF(x - label_size.width(), top, label_size.width(), height);
 }
 
+QMenu *Cursor::create_header_context_menu(QWidget *parent)
+{
+       QMenu *const menu = new QMenu(parent);
+
+       QAction *const snap_disable = new QAction(tr("Disable snapping"), this);
+       snap_disable->setCheckable(true);
+       snap_disable->setChecked(snapping_disabled_);
+       connect(snap_disable, &QAction::toggled, this, [=](bool checked){snapping_disabled_ = checked;});
+       menu->addAction(snap_disable);
+
+       return menu;
+}
+
 shared_ptr<Cursor> Cursor::get_other_cursor() const
 {
        const shared_ptr<CursorPair> cursors(view_.cursors());
index c3960d6d39aa57d5d3bd8c6a02773ccc14eb65e2..0da72e9fa5e3744b9d7c8f3ed2f1c84b3be02f71 100644 (file)
@@ -53,19 +53,21 @@ public:
        /**
         * Returns true if the item is visible and enabled.
         */
-       bool enabled() const;
+       virtual bool enabled() const override;
 
        /**
         * Gets the text to show in the marker.
         */
-       QString get_text() const;
+       virtual QString get_text() const override;
 
        /**
         * Gets the marker label rectangle.
         * @param rect The rectangle of the ruler client area.
         * @return Returns the label rectangle.
         */
-       QRectF label_rect(const QRectF &rect) const;
+       virtual QRectF label_rect(const QRectF &rect) const override;
+
+       virtual QMenu* create_header_context_menu(QWidget *parent) override;
 
 private:
        shared_ptr<Cursor> get_other_cursor() const;
index 933eb8fff33e0c3aaf2ed255dc2a6e00bc5b8754..7d7d8e4d6477d2e9efbd2a5b84f1bd3f52ec9d79 100644 (file)
@@ -21,6 +21,7 @@
 #include <cassert>
 
 #include <QColor>
+#include <QMenu>
 #include <QToolTip>
 
 #include "cursorpair.hpp"
@@ -45,13 +46,20 @@ const int CursorPair::DeltaPadding = 8;
 CursorPair::CursorPair(View &view) :
        TimeItem(view),
        first_(new Cursor(view, 0.0)),
-       second_(new Cursor(view, 1.0))
+       second_(new Cursor(view, 1.0)),
+       label_incomplete_(true)
 {
        GlobalSettings::add_change_handler(this);
 
        GlobalSettings settings;
        fill_color_ = QColor::fromRgba(settings.value(
                GlobalSettings::Key_View_CursorFillColor).value<uint32_t>());
+       show_frequency_ = settings.value(
+               GlobalSettings::Key_View_CursorShowFrequency).value<bool>();
+       show_interval_ = settings.value(
+               GlobalSettings::Key_View_CursorShowInterval).value<bool>();
+       show_samples_ = settings.value(
+               GlobalSettings::Key_View_CursorShowSamples).value<bool>();
 
        connect(&view_, SIGNAL(hover_point_changed(const QWidget*, QPoint)),
                this, SLOT(on_hover_point_changed(const QWidget*, QPoint)));
@@ -84,11 +92,24 @@ void CursorPair::set_time(const pv::util::Timestamp& time)
        second_->set_time(time + delta);
 }
 
+const pv::util::Timestamp CursorPair::time() const
+{
+       return 0;
+}
+
 float CursorPair::get_x() const
 {
        return (first_->get_x() + second_->get_x()) / 2.0f;
 }
 
+const pv::util::Timestamp CursorPair::delta(const pv::util::Timestamp& other) const
+{
+       if (other < second_->time())
+               return other - first_->time();
+       else
+               return other - second_->time();
+}
+
 QPoint CursorPair::drag_point(const QRect &rect) const
 {
        return first_->drag_point(rect);
@@ -100,6 +121,49 @@ pv::widgets::Popup* CursorPair::create_popup(QWidget *parent)
        return nullptr;
 }
 
+QMenu *CursorPair::create_header_context_menu(QWidget *parent)
+{
+       QMenu *menu = new QMenu(parent);
+
+       QAction *displayIntervalAction = new QAction(tr("Display interval"), this);
+       displayIntervalAction->setCheckable(true);
+       displayIntervalAction->setChecked(show_interval_);
+       menu->addAction(displayIntervalAction);
+
+       connect(displayIntervalAction, &QAction::toggled, displayIntervalAction,
+               [=]{
+                       GlobalSettings settings;
+                       settings.setValue(GlobalSettings::Key_View_CursorShowInterval,
+                               !settings.value(GlobalSettings::Key_View_CursorShowInterval).value<bool>());
+               });
+
+       QAction *displayFrequencyAction = new QAction(tr("Display frequency"), this);
+       displayFrequencyAction->setCheckable(true);
+       displayFrequencyAction->setChecked(show_frequency_);
+       menu->addAction(displayFrequencyAction);
+
+       connect(displayFrequencyAction, &QAction::toggled, displayFrequencyAction,
+               [=]{
+                       GlobalSettings settings;
+                       settings.setValue(GlobalSettings::Key_View_CursorShowFrequency,
+                               !settings.value(GlobalSettings::Key_View_CursorShowFrequency).value<bool>());
+               });
+
+       QAction *displaySamplesAction = new QAction(tr("Display samples"), this);
+       displaySamplesAction->setCheckable(true);
+       displaySamplesAction->setChecked(show_samples_);
+       menu->addAction(displaySamplesAction);
+
+       connect(displaySamplesAction, &QAction::toggled, displaySamplesAction,
+               [=]{
+                       GlobalSettings settings;
+                       settings.setValue(GlobalSettings::Key_View_CursorShowSamples,
+                               !settings.value(GlobalSettings::Key_View_CursorShowSamples).value<bool>());
+               });
+
+       return menu;
+}
+
 QRectF CursorPair::label_rect(const QRectF &rect) const
 {
        const QSizeF label_size(text_size_ + LabelPadding * 2);
@@ -129,19 +193,14 @@ void CursorPair::paint_label(QPainter &p, const QRect &rect, bool hover)
        const QColor text_color = ViewItem::select_text_color(Cursor::FillColor);
        p.setPen(text_color);
 
-       QString text = format_string();
-       text_size_ = p.boundingRect(QRectF(), 0, text).size();
-
        QRectF delta_rect(label_rect(rect));
        const int radius = delta_rect.height() / 2;
        QRectF text_rect(delta_rect.intersected(rect).adjusted(radius, 0, -radius, 0));
 
-       if (text_rect.width() < text_size_.width()) {
-               text = "...";
-               text_size_ = p.boundingRect(QRectF(), 0, text).size();
-               label_incomplete_ = true;
-       } else
-               label_incomplete_ = false;
+       QString text = format_string(text_rect.width(),
+               [&p](const QString& s) -> double { return p.boundingRect(QRectF(), 0, s).width(); });
+
+       text_size_ = p.boundingRect(QRectF(), 0, text).size();
 
        if (selected()) {
                p.setBrush(Qt::transparent);
@@ -178,17 +237,48 @@ void CursorPair::paint_back(QPainter &p, ViewItemPaintParams &pp)
        p.drawRect(l, pp.top(), r - l, pp.height());
 }
 
-QString CursorPair::format_string()
+QString CursorPair::format_string(int max_width, std::function<double(const QString&)> query_size)
 {
-       const pv::util::SIPrefix prefix = view_.tick_prefix();
-       const pv::util::Timestamp diff = abs(second_->time() - first_->time());
+       int time_precision = 12;
+       int freq_precision = 12;
+
+       QString s = format_string_sub(time_precision, freq_precision);
+
+       // Try full "{time} s / {freq} Hz" format
+       if ((max_width <= 0) || (query_size(s) <= max_width)) {
+               label_incomplete_ = false;
+               return s;
+       }
+
+       label_incomplete_ = true;
+
+       // Gradually reduce time precision to match frequency precision
+       while (time_precision > freq_precision) {
+               time_precision--;
 
-       const QString s1 = Ruler::format_time_with_distance(
-               diff, diff, prefix, view_.time_unit(), 12, false);  /* Always use 12 precision digits */
-       const QString s2 = util::format_time_si(
-               1 / diff, pv::util::SIPrefix::unspecified, 4, "Hz", false);
+               s = format_string_sub(time_precision, freq_precision);
+               if (query_size(s) <= max_width)
+                       return s;
+       }
+
+       // Gradually reduce both precisions down to zero
+       while (time_precision > 0) {
+               time_precision--;
+               freq_precision--;
 
-       return QString("%1 / %2").arg(s1, s2);
+               s = format_string_sub(time_precision, freq_precision);
+               if (query_size(s) <= max_width)
+                       return s;
+       }
+
+       // Try no trailing digits and drop the unit to at least display something
+       s = format_string_sub(0, 0, false);
+
+       if (query_size(s) <= max_width)
+               return s;
+
+       // Give up
+       return "...";
 }
 
 pair<float, float> CursorPair::get_cursor_offsets() const
@@ -203,6 +293,15 @@ void CursorPair::on_setting_changed(const QString &key, const QVariant &value)
 {
        if (key == GlobalSettings::Key_View_CursorFillColor)
                fill_color_ = QColor::fromRgba(value.value<uint32_t>());
+
+       if (key == GlobalSettings::Key_View_CursorShowFrequency)
+               show_frequency_ = value.value<bool>();
+
+       if (key == GlobalSettings::Key_View_CursorShowInterval)
+               show_interval_ = value.value<bool>();
+
+       if (key == GlobalSettings::Key_View_CursorShowSamples)
+               show_samples_ = value.value<bool>();
 }
 
 void CursorPair::on_hover_point_changed(const QWidget* widget, const QPoint& hp)
@@ -219,6 +318,52 @@ void CursorPair::on_hover_point_changed(const QWidget* widget, const QPoint& hp)
                QToolTip::hideText();  // TODO Will break other tooltips when there can be others
 }
 
+QString CursorPair::format_string_sub(int time_precision, int freq_precision, bool show_unit)
+{
+       QString s = " ";
+
+       const pv::util::SIPrefix prefix = view_.tick_prefix();
+       const pv::util::Timestamp diff = abs(second_->time() - first_->time());
+
+       const QString time = Ruler::format_time_with_distance(
+               diff, diff, prefix, (show_unit ? view_.time_unit() : pv::util::TimeUnit::None),
+               time_precision, false);
+
+       // We can only show a frequency when there's a time base
+       if (view_.time_unit() == pv::util::TimeUnit::Time) {
+               int items = 0;
+
+               if (show_frequency_) {
+                       const QString freq = util::format_value_si(
+                               1 / diff.convert_to<double>(), pv::util::SIPrefix::unspecified,
+                               freq_precision, (show_unit ? "Hz" : nullptr), false);
+                       s = QString("%1").arg(freq);
+                       items++;
+               }
+
+               if (show_interval_) {
+                       if (items > 0)
+                               s = QString("%1 / %2").arg(s, time);
+                       else
+                               s = QString("%1").arg(time);
+                       items++;
+               }
+
+               if (show_samples_) {
+                       const QString samples = QString::number(
+                               (diff * view_.session().get_samplerate()).convert_to<uint64_t>());
+                       if (items > 0)
+                               s = QString("%1 / %2").arg(s, samples);
+                       else
+                               s = QString("%1").arg(samples);
+               }
+       } else
+               // In this case, we return the number of samples, really
+               s = time;
+
+       return s;
+}
+
 } // namespace trace
 } // namespace views
 } // namespace pv
index 9d450df6b69af06e1dcc9a0830f036c9b18c3f23..d59d9414d6ae5488896131ccf9c667716bd0737f 100644 (file)
@@ -76,12 +76,18 @@ public:
         */
        void set_time(const pv::util::Timestamp& time) override;
 
+       virtual const pv::util::Timestamp time() const override;
+
        float get_x() const override;
 
+       virtual const pv::util::Timestamp delta(const pv::util::Timestamp& other) const override;
+
        QPoint drag_point(const QRect &rect) const override;
 
        pv::widgets::Popup* create_popup(QWidget *parent) override;
 
+       QMenu* create_header_context_menu(QWidget *parent) override;
+
        QRectF label_rect(const QRectF &rect) const override;
 
        /**
@@ -102,7 +108,8 @@ public:
        /**
         * Constructs the string to display.
         */
-       QString format_string();
+       QString format_string(int max_width = 0, std::function<double(const QString&)> query_size
+               = [](const QString& s) -> double { (void)s; return 0; });
 
        pair<float, float> get_cursor_offsets() const;
 
@@ -111,6 +118,9 @@ public:
 public Q_SLOTS:
        void on_hover_point_changed(const QWidget* widget, const QPoint &hp);
 
+private:
+       QString format_string_sub(int time_precision, int freq_precision, bool show_unit = true);
+
 private:
        shared_ptr<Cursor> first_, second_;
        QColor fill_color_;
@@ -118,6 +128,7 @@ private:
        QSizeF text_size_;
        QRectF label_area_;
        bool label_incomplete_;
+       bool show_interval_, show_frequency_, show_samples_;
 };
 
 } // namespace trace
index 9c7196bfb04f24e1c3322172ce023d12f12f4b36..1cc89feb67ae46af816a036b26d97e55d9a3ec87 100644 (file)
@@ -31,7 +31,10 @@ extern "C" {
 
 #include <QAction>
 #include <QApplication>
+#include <QClipboard>
+#include <QCheckBox>
 #include <QComboBox>
+#include <QDebug>
 #include <QFileDialog>
 #include <QFormLayout>
 #include <QLabel>
@@ -55,56 +58,117 @@ extern "C" {
 #include <pv/data/logicsegment.hpp>
 #include <pv/widgets/decodergroupbox.hpp>
 #include <pv/widgets/decodermenu.hpp>
+#include <pv/widgets/flowlayout.hpp>
 
 using std::abs;
+using std::find_if;
+using std::lock_guard;
 using std::make_pair;
 using std::max;
 using std::min;
 using std::numeric_limits;
-using std::out_of_range;
 using std::pair;
 using std::shared_ptr;
 using std::tie;
 using std::vector;
 
 using pv::data::decode::Annotation;
+using pv::data::decode::AnnotationClass;
 using pv::data::decode::Row;
-using pv::data::DecodeChannel;
+using pv::data::decode::DecodeChannel;
 using pv::data::DecodeSignal;
 
 namespace pv {
 namespace views {
 namespace trace {
 
-
 #define DECODETRACE_COLOR_SATURATION (180) /* 0-255 */
 #define DECODETRACE_COLOR_VALUE (170) /* 0-255 */
 
 const QColor DecodeTrace::ErrorBgColor = QColor(0xEF, 0x29, 0x29);
 const QColor DecodeTrace::NoDecodeColor = QColor(0x88, 0x8A, 0x85);
+const QColor DecodeTrace::ExpandMarkerWarnColor = QColor(0xFF, 0xA5, 0x00); // QColorConstants::Svg::orange
+const QColor DecodeTrace::ExpandMarkerHiddenColor = QColor(0x69, 0x69, 0x69); // QColorConstants::Svg::dimgray
+const uint8_t DecodeTrace::ExpansionAreaHeaderAlpha = 10 * 255 / 100;
+const uint8_t DecodeTrace::ExpansionAreaAlpha = 5 * 255 / 100;
 
-const int DecodeTrace::ArrowSize = 4;
+const int DecodeTrace::ArrowSize = 6;
 const double DecodeTrace::EndCapWidth = 5;
-const int DecodeTrace::RowTitleMargin = 10;
+const int DecodeTrace::RowTitleMargin = 7;
 const int DecodeTrace::DrawPadding = 100;
 
 const int DecodeTrace::MaxTraceUpdateRate = 1; // No more than 1 Hz
+const int DecodeTrace::AnimationDurationInTicks = 7;
+const int DecodeTrace::HiddenRowHideDelay = 1000; // 1 second
+
+/**
+ * Helper function for forceUpdate()
+ */
+void invalidateLayout(QLayout* layout)
+{
+       // Recompute the given layout and all its child layouts recursively
+       for (int i = 0; i < layout->count(); i++) {
+               QLayoutItem *item = layout->itemAt(i);
+
+               if (item->layout())
+                       invalidateLayout(item->layout());
+               else
+                       item->invalidate();
+       }
+
+       layout->invalidate();
+       layout->activate();
+}
+
+void forceUpdate(QWidget* widget)
+{
+       // Update all child widgets recursively
+       for (QObject* child : widget->children())
+               if (child->isWidgetType())
+                       forceUpdate((QWidget*)child);
+
+       // Invalidate the layout of the widget itself
+       if (widget->layout())
+               invalidateLayout(widget->layout());
+}
+
+
+ContainerWidget::ContainerWidget(QWidget *parent) :
+       QWidget(parent)
+{
+}
+
+void ContainerWidget::resizeEvent(QResizeEvent* event)
+{
+       QWidget::resizeEvent(event);
+
+       widgetResized(this);
+}
+
 
 DecodeTrace::DecodeTrace(pv::Session &session,
        shared_ptr<data::SignalBase> signalbase, int index) :
        Trace(signalbase),
        session_(session),
-       row_height_(0),
-       max_visible_rows_(0),
+       show_hidden_rows_(false),
        delete_mapper_(this),
-       show_hide_mapper_(this)
+       show_hide_mapper_(this),
+       row_show_hide_mapper_(this)
 {
        decode_signal_ = dynamic_pointer_cast<data::DecodeSignal>(base_);
 
+       GlobalSettings settings;
+       always_show_all_rows_ = settings.value(GlobalSettings::Key_Dec_AlwaysShowAllRows).toBool();
+
+       GlobalSettings::add_change_handler(this);
+
        // Determine shortest string we want to see displayed in full
        QFontMetrics m(QApplication::font());
        min_useful_label_width_ = m.width("XX"); // e.g. two hex characters
 
+       default_row_height_ = (ViewItemPaintParams::text_height() * 6) / 4;
+       annotation_height_ = (ViewItemPaintParams::text_height() * 5) / 4;
+
        // For the base color, we want to start at a very different color for
        // every decoder stack, so multiply the index with a number that is
        // rather close to 180 degrees of the color circle but not a dividend of 360
@@ -129,11 +193,42 @@ DecodeTrace::DecodeTrace(pv::Session &session,
                this, SLOT(on_delete_decoder(int)));
        connect(&show_hide_mapper_, SIGNAL(mapped(int)),
                this, SLOT(on_show_hide_decoder(int)));
+       connect(&row_show_hide_mapper_, SIGNAL(mapped(int)),
+               this, SLOT(on_show_hide_row(int)));
+       connect(&class_show_hide_mapper_, SIGNAL(mapped(QWidget*)),
+               this, SLOT(on_show_hide_class(QWidget*)));
 
        connect(&delayed_trace_updater_, SIGNAL(timeout()),
                this, SLOT(on_delayed_trace_update()));
        delayed_trace_updater_.setSingleShot(true);
        delayed_trace_updater_.setInterval(1000 / MaxTraceUpdateRate);
+
+       connect(&animation_timer_, SIGNAL(timeout()),
+               this, SLOT(on_animation_timer()));
+       animation_timer_.setInterval(1000 / 50);
+
+       connect(&delayed_hidden_row_hider_, SIGNAL(timeout()),
+               this, SLOT(on_hide_hidden_rows()));
+       delayed_hidden_row_hider_.setSingleShot(true);
+       delayed_hidden_row_hider_.setInterval(HiddenRowHideDelay);
+
+       default_marker_shape_ << QPoint(0,         -ArrowSize);
+       default_marker_shape_ << QPoint(ArrowSize,  0);
+       default_marker_shape_ << QPoint(0,          ArrowSize);
+}
+
+DecodeTrace::~DecodeTrace()
+{
+       GlobalSettings::remove_change_handler(this);
+
+       for (DecodeTraceRow& r : rows_) {
+               for (QCheckBox* cb : r.selectors)
+                       delete cb;
+
+               delete r.selector_container;
+               delete r.header_container;
+               delete r.container;
+       }
 }
 
 bool DecodeTrace::enabled() const
@@ -146,14 +241,29 @@ shared_ptr<data::SignalBase> DecodeTrace::base() const
        return base_;
 }
 
-pair<int, int> DecodeTrace::v_extents() const
+void DecodeTrace::set_owner(TraceTreeItemOwner *owner)
 {
-       const int row_height = (ViewItemPaintParams::text_height() * 6) / 4;
+       Trace::set_owner(owner);
 
+       // The owner is set in trace::View::signals_changed(), which is a slot.
+       // So after this trace was added to the view, we won't have an owner
+       // that we need to initialize in update_rows(). Once we do, we call it
+       // from on_decode_reset().
+       on_decode_reset();
+}
+
+pair<int, int> DecodeTrace::v_extents() const
+{
        // Make an empty decode trace appear symmetrical
-       const int row_count = max(1, max_visible_rows_);
+       if (visible_rows_ == 0)
+               return make_pair(-default_row_height_, default_row_height_);
+
+       unsigned int height = 0;
+       for (const DecodeTraceRow& r : rows_)
+               if (r.currently_visible)
+                       height += r.height;
 
-       return make_pair(-row_height, row_height * row_count);
+       return make_pair(-default_row_height_, height);
 }
 
 void DecodeTrace::paint_back(QPainter &p, ViewItemPaintParams &pp)
@@ -164,15 +274,16 @@ void DecodeTrace::paint_back(QPainter &p, ViewItemPaintParams &pp)
 
 void DecodeTrace::paint_mid(QPainter &p, ViewItemPaintParams &pp)
 {
-       const int text_height = ViewItemPaintParams::text_height();
-       row_height_ = (text_height * 6) / 4;
-       const int annotation_height = (text_height * 5) / 4;
+       lock_guard<mutex> lock(row_modification_mutex_);
+       unsigned int visible_rows;
+
+#if DECODETRACE_SHOW_RENDER_TIME
+       render_time_.restart();
+#endif
 
        // Set default pen to allow for text width calculation
        p.setPen(Qt::black);
 
-       // Iterate through the rows
-       int y = get_visual_y();
        pair<uint64_t, uint64_t> sample_range = get_view_sample_range(pp.left(), pp.right());
 
        // Just because the view says we see a certain sample range it
@@ -181,36 +292,44 @@ void DecodeTrace::paint_mid(QPainter &p, ViewItemPaintParams &pp)
        sample_range.second = min((int64_t)sample_range.second,
                decode_signal_->get_decoded_sample_count(current_segment_, false));
 
-       const vector<Row> rows = decode_signal_->visible_rows();
-
-       visible_rows_.clear();
-       for (const Row& row : rows) {
-               // Cache the row title widths
-               int row_title_width;
-               try {
-                       row_title_width = row_title_widths_.at(row);
-               } catch (out_of_range&) {
-                       const int w = p.boundingRect(QRectF(), 0, row.title()).width() +
-                               RowTitleMargin;
-                       row_title_widths_[row] = w;
-                       row_title_width = w;
+       visible_rows = 0;
+       int y = get_visual_y();
+
+       for (DecodeTraceRow& r : rows_) {
+               // If the row is hidden, we don't want to fetch annotations
+               assert(r.decode_row);
+               assert(r.decode_row->decoder());
+               if ((!r.decode_row->decoder()->visible()) ||
+                       ((!r.decode_row->visible() && (!show_hidden_rows_) && (!r.expanding) && (!r.expanded) && (!r.collapsing)))) {
+                       r.currently_visible = false;
+                       continue;
                }
 
-               vector<Annotation> annotations;
-               decode_signal_->get_annotation_subset(annotations, row,
+               deque<const Annotation*> annotations;
+               decode_signal_->get_annotation_subset(annotations, r.decode_row,
                        current_segment_, sample_range.first, sample_range.second);
-               if (!annotations.empty()) {
-                       draw_annotations(annotations, p, annotation_height, pp, y,
-                               get_row_color(row.index()), row_title_width);
-                       y += row_height_;
-                       visible_rows_.push_back(row);
+
+               // Show row if there are visible annotations, when user wants to see
+               // all rows that have annotations somewhere and this one is one of them
+               // or when the row has at least one hidden annotation class
+               r.currently_visible = !annotations.empty();
+               if (!r.currently_visible) {
+                       size_t ann_count = decode_signal_->get_annotation_count(r.decode_row, current_segment_);
+                       r.currently_visible = ((always_show_all_rows_ || r.has_hidden_classes) &&
+                               (ann_count > 0)) || r.expanded;
+               }
+
+               if (r.currently_visible) {
+                       draw_annotations(annotations, p, pp, y, r);
+                       y += r.height;
+                       visible_rows++;
                }
        }
 
-       draw_unresolved_period(p, annotation_height, pp.left(), pp.right());
+       draw_unresolved_period(p, pp.left(), pp.right());
 
-       if ((int)visible_rows_.size() > max_visible_rows_) {
-               max_visible_rows_ = (int)visible_rows_.size();
+       if (visible_rows != visible_rows_) {
+               visible_rows_ = visible_rows;
 
                // Call order is important, otherwise the lazy event handler won't work
                owner_->extents_changed(false, true);
@@ -220,30 +339,43 @@ void DecodeTrace::paint_mid(QPainter &p, ViewItemPaintParams &pp)
        const QString err = decode_signal_->error_message();
        if (!err.isEmpty())
                draw_error(p, err, pp);
+
+#if DECODETRACE_SHOW_RENDER_TIME
+       qDebug() << "Rendering" << base_->name() << "took" << render_time_.elapsed() << "ms";
+#endif
 }
 
 void DecodeTrace::paint_fore(QPainter &p, ViewItemPaintParams &pp)
 {
-       assert(row_height_);
+       unsigned int y = get_visual_y();
 
-       for (size_t i = 0; i < visible_rows_.size(); i++) {
-               const int y = i * row_height_ + get_visual_y();
+       update_expanded_rows();
+
+       for (const DecodeTraceRow& r : rows_) {
+               if (!r.currently_visible)
+                       continue;
 
                p.setPen(QPen(Qt::NoPen));
-               p.setBrush(QApplication::palette().brush(QPalette::WindowText));
 
-               if (i != 0) {
-                       const QPointF points[] = {
-                               QPointF(pp.left(), y - ArrowSize),
-                               QPointF(pp.left() + ArrowSize, y),
-                               QPointF(pp.left(), y + ArrowSize)
-                       };
-                       p.drawPolygon(points, countof(points));
-               }
+               if (r.expand_marker_highlighted)
+                       p.setBrush(QApplication::palette().brush(QPalette::Highlight));
+               else if (!r.decode_row->visible())
+                       p.setBrush(ExpandMarkerHiddenColor);
+               else if (r.has_hidden_classes)
+                       p.setBrush(ExpandMarkerWarnColor);
+               else
+                       p.setBrush(QApplication::palette().brush(QPalette::WindowText));
 
-               const QRect r(pp.left() + ArrowSize * 2, y - row_height_ / 2,
-                       pp.right() - pp.left(), row_height_);
-               const QString h(visible_rows_[i].title());
+               // Draw expansion marker
+               QPolygon marker(r.expand_marker_shape);
+               marker.translate(pp.left(), y);
+               p.drawPolygon(marker);
+
+               p.setBrush(QApplication::palette().brush(QPalette::WindowText));
+
+               const QRect text_rect(pp.left() + ArrowSize * 2, y - r.height / 2,
+                       pp.right() - pp.left(), r.height);
+               const QString h(r.decode_row->title());
                const int f = Qt::AlignLeft | Qt::AlignVCenter |
                        Qt::TextDontClip;
 
@@ -252,21 +384,52 @@ void DecodeTrace::paint_fore(QPainter &p, ViewItemPaintParams &pp)
                for (int dx = -1; dx <= 1; dx++)
                        for (int dy = -1; dy <= 1; dy++)
                                if (dx != 0 && dy != 0)
-                                       p.drawText(r.translated(dx, dy), f, h);
+                                       p.drawText(text_rect.translated(dx, dy), f, h);
 
                // Draw the text
-               p.setPen(QApplication::palette().color(QPalette::WindowText));
-               p.drawText(r, f, h);
+               if (!r.decode_row->visible())
+                       p.setPen(ExpandMarkerHiddenColor);
+               else
+                       p.setPen(QApplication::palette().color(QPalette::WindowText));
+
+               p.drawText(text_rect, f, h);
+
+               y += r.height;
        }
 
        if (show_hover_marker_)
                paint_hover_marker(p);
 }
 
-void DecodeTrace::populate_popup_form(QWidget *parent, QFormLayout *form)
+void DecodeTrace::update_stack_button()
 {
-       using pv::data::decode::Decoder;
+       const vector< shared_ptr<Decoder> > &stack = decode_signal_->decoder_stack();
+
+       // Only show decoders in the menu that can be stacked onto the last one in the stack
+       if (!stack.empty()) {
+               const srd_decoder* d = stack.back()->get_srd_decoder();
+
+               if (d->outputs) {
+                       pv::widgets::DecoderMenu *const decoder_menu =
+                               new pv::widgets::DecoderMenu(stack_button_, (const char*)(d->outputs->data));
+                       connect(decoder_menu, SIGNAL(decoder_selected(srd_decoder*)),
+                               this, SLOT(on_stack_decoder(srd_decoder*)));
+
+                       decoder_menu->setStyleSheet("QMenu { menu-scrollable: 1; }");
+
+                       stack_button_->setMenu(decoder_menu);
+                       stack_button_->show();
+                       return;
+               }
+       }
+
+       // No decoders available for stacking
+       stack_button_->setMenu(nullptr);
+       stack_button_->hide();
+}
 
+void DecodeTrace::populate_popup_form(QWidget *parent, QFormLayout *form)
+{
        assert(form);
 
        // Add the standard options
@@ -297,18 +460,12 @@ void DecodeTrace::populate_popup_form(QWidget *parent, QFormLayout *form)
        }
 
        // Add stacking button
-       pv::widgets::DecoderMenu *const decoder_menu =
-               new pv::widgets::DecoderMenu(parent);
-       connect(decoder_menu, SIGNAL(decoder_selected(srd_decoder*)),
-               this, SLOT(on_stack_decoder(srd_decoder*)));
-
-       QPushButton *const stack_button =
-               new QPushButton(tr("Stack Decoder"), parent);
-       stack_button->setMenu(decoder_menu);
-       stack_button->setToolTip(tr("Stack a higher-level decoder on top of this one"));
+       stack_button_ = new QPushButton(tr("Stack Decoder"), parent);
+       stack_button_->setToolTip(tr("Stack a higher-level decoder on top of this one"));
+       update_stack_button();
 
        QHBoxLayout *stack_button_box = new QHBoxLayout;
-       stack_button_box->addWidget(stack_button, 0, Qt::AlignRight);
+       stack_button_box->addWidget(stack_button_, 0, Qt::AlignRight);
        form->addRow(stack_button_box);
 }
 
@@ -345,15 +502,17 @@ QMenu* DecodeTrace::create_view_context_menu(QWidget *parent, QPoint &click_pos)
                        menu->addSeparator();
        }
 
-       try {
-               selected_row_ = &visible_rows_[get_row_at_point(click_pos)];
-       } catch (out_of_range&) {
-               selected_row_ = nullptr;
-       }
+       selected_row_ = nullptr;
+       const DecodeTraceRow* r = get_row_at_point(click_pos);
+       if (r)
+               selected_row_ = r->decode_row;
+
+       const View *const view = owner_->view();
+       assert(view);
+       QPoint pos = view->viewport()->mapFrom(parent, click_pos);
 
        // Default sample range is "from here"
-       const pair<uint64_t, uint64_t> sample_range =
-               get_view_sample_range(click_pos.x(), click_pos.x() + 1);
+       const pair<uint64_t, uint64_t> sample_range = get_view_sample_range(pos.x(), pos.x() + 1);
        selected_sample_range_ = make_pair(sample_range.first, numeric_limits<uint64_t>::max());
 
        if (decode_signal_->is_paused()) {
@@ -372,6 +531,13 @@ QMenu* DecodeTrace::create_view_context_menu(QWidget *parent, QPoint &click_pos)
                menu->addAction(pause);
        }
 
+       QAction *const copy_annotation_to_clipboard =
+               new QAction(tr("Copy annotation text to clipboard"), this);
+       copy_annotation_to_clipboard->setIcon(QIcon::fromTheme("edit-paste",
+               QIcon(":/icons/edit-paste.svg")));
+       connect(copy_annotation_to_clipboard, SIGNAL(triggered()), this, SLOT(on_copy_annotation_to_clipboard()));
+       menu->addAction(copy_annotation_to_clipboard);
+
        menu->addSeparator();
 
        QAction *const export_all_rows =
@@ -420,9 +586,6 @@ QMenu* DecodeTrace::create_view_context_menu(QWidget *parent, QPoint &click_pos)
        connect(export_row_with_cursor, SIGNAL(triggered()), this, SLOT(on_export_row_with_cursor()));
        menu->addAction(export_row_with_cursor);
 
-       const View *view = owner_->view();
-       assert(view);
-
        if (!view->cursors()->enabled()) {
                export_all_rows_with_cursor->setEnabled(false);
                export_row_with_cursor->setEnabled(false);
@@ -431,18 +594,115 @@ QMenu* DecodeTrace::create_view_context_menu(QWidget *parent, QPoint &click_pos)
        return menu;
 }
 
-void DecodeTrace::draw_annotations(vector<pv::data::decode::Annotation> annotations,
-               QPainter &p, int h, const ViewItemPaintParams &pp, int y,
-               QColor row_color, int row_title_width)
+void DecodeTrace::delete_pressed()
+{
+       on_delete();
+}
+
+void DecodeTrace::hover_point_changed(const QPoint &hp)
+{
+       Trace::hover_point_changed(hp);
+
+       assert(owner_);
+
+       DecodeTraceRow* hover_row = get_row_at_point(hp);
+
+       // Row expansion marker handling
+       for (DecodeTraceRow& r : rows_)
+               r.expand_marker_highlighted = false;
+
+       if (hover_row) {
+               int row_y = get_row_y(hover_row);
+               if ((hp.x() > 0) && (hp.x() < (int)(ArrowSize + 3 + hover_row->title_width)) &&
+                       (hp.y() > (int)(row_y - ArrowSize)) && (hp.y() < (int)(row_y + ArrowSize))) {
+
+                       hover_row->expand_marker_highlighted = true;
+                       show_hidden_rows_ = true;
+                       delayed_hidden_row_hider_.start();
+               }
+       }
+
+       // Tooltip handling
+       if (hp.x() > 0) {
+               QString ann = get_annotation_at_point(hp);
+
+               if (!ann.isEmpty()) {
+                       QFontMetrics m(QToolTip::font());
+                       const QRect text_size = m.boundingRect(QRect(), 0, ann);
+
+                       // This is OS-specific and unfortunately we can't query it, so
+                       // use an approximation to at least try to minimize the error.
+                       const int padding = default_row_height_ + 8;
+
+                       // Make sure the tool tip doesn't overlap with the mouse cursor.
+                       // If it did, the tool tip would constantly hide and re-appear.
+                       // We also push it up by one row so that it appears above the
+                       // decode trace, not below.
+                       QPoint p = hp;
+                       p.setX(hp.x() - (text_size.width() / 2) - padding);
+
+                       p.setY(get_row_y(hover_row) - default_row_height_ -
+                               text_size.height() - padding);
+
+                       const View *const view = owner_->view();
+                       assert(view);
+                       QToolTip::showText(view->viewport()->mapToGlobal(p), ann);
+
+               } else
+                       QToolTip::hideText();
+
+       } else
+               QToolTip::hideText();
+}
+
+void DecodeTrace::mouse_left_press_event(const QMouseEvent* event)
 {
-       using namespace pv::data::decode;
+       // Update container widths which depend on the scrollarea's current width
+       update_expanded_rows();
+
+       // Handle row expansion marker
+       for (DecodeTraceRow& r : rows_) {
+               if (!r.expand_marker_highlighted)
+                       continue;
+
+               unsigned int y = get_row_y(&r);
+               if ((event->x() > 0) && (event->x() <= (int)(ArrowSize + 3 + r.title_width)) &&
+                       (event->y() > (int)(y - (default_row_height_ / 2))) &&
+                       (event->y() <= (int)(y + (default_row_height_ / 2)))) {
+
+                       if (r.expanded) {
+                               r.collapsing = true;
+                               r.expanded = false;
+                               r.anim_shape = ArrowSize;
+                       } else {
+                               r.expanding = true;
+                               r.anim_shape = 0;
+
+                               // Force geometry update of the widget container to get
+                               // an up-to-date height (which also depends on the width)
+                               forceUpdate(r.container);
+
+                               r.container->setVisible(true);
+                               r.expanded_height = 2 * default_row_height_ + r.container->sizeHint().height();
+                       }
+
+                       r.animation_step = 0;
+                       r.anim_height = r.height;
+
+                       animation_timer_.start();
+               }
+       }
+}
 
+void DecodeTrace::draw_annotations(deque<const Annotation*>& annotations,
+               QPainter &p, const ViewItemPaintParams &pp, int y, const DecodeTraceRow& row)
+{
        Annotation::Class block_class = 0;
        bool block_class_uniform = true;
        qreal block_start = 0;
        int block_ann_count = 0;
 
-       const Annotation *prev_ann;
+       const Annotationprev_ann;
        qreal prev_end = INT_MIN;
 
        qreal a_end;
@@ -451,17 +711,11 @@ void DecodeTrace::draw_annotations(vector<pv::data::decode::Annotation> annotati
        tie(pixels_offset, samples_per_pixel) =
                get_pixels_offset_samples_per_pixel();
 
-       // Sort the annotations by start sample so that decoders
-       // can't confuse us by creating annotations out of order
-       stable_sort(annotations.begin(), annotations.end(),
-               [](const Annotation &a, const Annotation &b) {
-                       return a.start_sample() < b.start_sample(); });
-
        // Gather all annotations that form a visual "block" and draw them as such
-       for (const Annotation &a : annotations) {
+       for (const Annotationa : annotations) {
 
-               const qreal abs_a_start = a.start_sample() / samples_per_pixel;
-               const qreal abs_a_end   = a.end_sample() / samples_per_pixel;
+               const qreal abs_a_start = a->start_sample() / samples_per_pixel;
+               const qreal abs_a_end   = a->end_sample() / samples_per_pixel;
 
                const qreal a_start = abs_a_start - pixels_offset;
                a_end = abs_a_end - pixels_offset;
@@ -473,7 +727,7 @@ void DecodeTrace::draw_annotations(vector<pv::data::decode::Annotation> annotati
 
                // Annotation wider than the threshold for a useful label width?
                if (a_width >= min_useful_label_width_) {
-                       for (const QString &ann_text : a.annotations()) {
+                       for (const QString &ann_text : *(a->annotations())) {
                                const qreal w = p.boundingRect(QRectF(), 0, ann_text).width();
                                // Annotation wide enough to fit a label? Don't put it in a block then
                                if (w <= a_width) {
@@ -487,17 +741,16 @@ void DecodeTrace::draw_annotations(vector<pv::data::decode::Annotation> annotati
                if ((abs(delta) > 1) || a_is_separate) {
                        // Block was broken, draw annotations that form the current block
                        if (block_ann_count == 1)
-                               draw_annotation(*prev_ann, p, h, pp, y, row_color,
-                                       row_title_width);
+                               draw_annotation(prev_ann, p, pp, y, row);
                        else if (block_ann_count > 0)
                                draw_annotation_block(block_start, prev_end, block_class,
-                                       block_class_uniform, p, h, y, row_color);
+                                       block_class_uniform, p, y, row);
 
                        block_ann_count = 0;
                }
 
                if (a_is_separate) {
-                       draw_annotation(a, p, h, pp, y, row_color, row_title_width);
+                       draw_annotation(a, p, pp, y, row);
                        // Next annotation must start a new block. delta will be > 1
                        // because we set prev_end to INT_MIN but that's okay since
                        // block_ann_count will be 0 and nothing will be drawn
@@ -505,14 +758,14 @@ void DecodeTrace::draw_annotations(vector<pv::data::decode::Annotation> annotati
                        block_ann_count = 0;
                } else {
                        prev_end = a_end;
-                       prev_ann = &a;
+                       prev_ann = a;
 
                        if (block_ann_count == 0) {
                                block_start = a_start;
-                               block_class = a.ann_class();
+                               block_class = a->ann_class_id();
                                block_class_uniform = true;
                        } else
-                               if (a.ann_class() != block_class)
+                               if (a->ann_class_id() != block_class)
                                        block_class_uniform = false;
 
                        block_ann_count++;
@@ -520,88 +773,83 @@ void DecodeTrace::draw_annotations(vector<pv::data::decode::Annotation> annotati
        }
 
        if (block_ann_count == 1)
-               draw_annotation(*prev_ann, p, h, pp, y, row_color, row_title_width);
+               draw_annotation(prev_ann, p, pp, y, row);
        else if (block_ann_count > 0)
                draw_annotation_block(block_start, prev_end, block_class,
-                       block_class_uniform, p, h, y, row_color);
+                       block_class_uniform, p, y, row);
 }
 
-void DecodeTrace::draw_annotation(const pv::data::decode::Annotation &a,
-       QPainter &p, int h, const ViewItemPaintParams &pp, int y,
-       QColor row_color, int row_title_width) const
+void DecodeTrace::draw_annotation(const Annotation* a, QPainter &p,
+       const ViewItemPaintParams &pp, int y, const DecodeTraceRow& row) const
 {
        double samples_per_pixel, pixels_offset;
        tie(pixels_offset, samples_per_pixel) =
                get_pixels_offset_samples_per_pixel();
 
-       const double start = a.start_sample() / samples_per_pixel -
-               pixels_offset;
-       const double end = a.end_sample() / samples_per_pixel - pixels_offset;
+       const double start = a->start_sample() / samples_per_pixel - pixels_offset;
+       const double end = a->end_sample() / samples_per_pixel - pixels_offset;
 
-       QColor color = get_annotation_color(row_color, a.ann_class());
-       p.setPen(color.darker());
-       p.setBrush(color);
+       p.setPen(row.ann_class_dark_color.at(a->ann_class_id()));
+       p.setBrush(row.ann_class_color.at(a->ann_class_id()));
 
-       if (start > pp.right() + DrawPadding || end < pp.left() - DrawPadding)
+       if ((start > (pp.right() + DrawPadding)) || (end < (pp.left() - DrawPadding)))
                return;
 
-       if (a.start_sample() == a.end_sample())
-               draw_instant(a, p, h, start, y);
+       if (a->start_sample() == a->end_sample())
+               draw_instant(a, p, start, y);
        else
-               draw_range(a, p, h, start, end, y, pp, row_title_width);
+               draw_range(a, p, start, end, y, pp, row.title_width);
 }
 
 void DecodeTrace::draw_annotation_block(qreal start, qreal end,
-       Annotation::Class ann_class, bool use_ann_format, QPainter &p, int h,
-       int y, QColor row_color) const
+       Annotation::Class ann_class, bool use_ann_format, QPainter &p, int y,
+       const DecodeTraceRow& row) const
 {
-       const double top = y + .5 - h / 2;
-       const double bottom = y + .5 + h / 2;
-
-       const QRectF rect(start, top, end - start, bottom - top);
-       const int r = h / 4;
-
-       p.setPen(QPen(Qt::NoPen));
-       p.setBrush(Qt::white);
-       p.drawRoundedRect(rect, r, r);
+       const double top = y + .5 - annotation_height_ / 2;
+       const double bottom = y + .5 + annotation_height_ / 2;
+       const double width = end - start;
 
        // If all annotations in this block are of the same type, we can use the
        // one format that all of these annotations have. Otherwise, we should use
        // a neutral color (i.e. gray)
        if (use_ann_format) {
-               const QColor color = get_annotation_color(row_color, ann_class);
-               p.setPen(color.darker());
-               p.setBrush(QBrush(color, Qt::Dense4Pattern));
+               p.setPen(row.ann_class_dark_color.at(ann_class));
+               p.setBrush(QBrush(row.ann_class_color.at(ann_class), Qt::Dense4Pattern));
        } else {
-               p.setPen(Qt::gray);
+               p.setPen(QColor(Qt::darkGray));
                p.setBrush(QBrush(Qt::gray, Qt::Dense4Pattern));
        }
 
-       p.drawRoundedRect(rect, r, r);
+       if (width <= 1)
+               p.drawLine(QPointF(start, top), QPointF(start, bottom));
+       else {
+               const QRectF rect(start, top, width, bottom - top);
+               const int r = annotation_height_ / 4;
+               p.drawRoundedRect(rect, r, r);
+       }
 }
 
-void DecodeTrace::draw_instant(const pv::data::decode::Annotation &a, QPainter &p,
-       int h, qreal x, int y) const
+void DecodeTrace::draw_instant(const Annotation* a, QPainter &p, qreal x, int y) const
 {
-       const QString text = a.annotations().empty() ?
-               QString() : a.annotations().back();
+       const QString text = a->annotations()->empty() ?
+               QString() : a->annotations()->back();
        const qreal w = min((qreal)p.boundingRect(QRectF(), 0, text).width(),
-               0.0) + h;
-       const QRectF rect(x - w / 2, y - h / 2, w, h);
+               0.0) + annotation_height_;
+       const QRectF rect(x - w / 2, y - annotation_height_ / 2, w, annotation_height_);
 
-       p.drawRoundedRect(rect, h / 2, h / 2);
+       p.drawRoundedRect(rect, annotation_height_ / 2, annotation_height_ / 2);
 
        p.setPen(Qt::black);
        p.drawText(rect, Qt::AlignCenter | Qt::AlignVCenter, text);
 }
 
-void DecodeTrace::draw_range(const pv::data::decode::Annotation &a, QPainter &p,
-       int h, qreal start, qreal end, int y, const ViewItemPaintParams &pp,
+void DecodeTrace::draw_range(const Annotation* a, QPainter &p,
+       qreal start, qreal end, int y, const ViewItemPaintParams &pp,
        int row_title_width) const
 {
-       const qreal top = y + .5 - h / 2;
-       const qreal bottom = y + .5 + h / 2;
-       const vector<QString> annotations = a.annotations();
+       const qreal top = y + .5 - annotation_height_ / 2;
+       const qreal bottom = y + .5 + annotation_height_ / 2;
+       const vector<QString>* annotations = a->annotations();
 
        // If the two ends are within 1 pixel, draw a vertical line
        if (start + 1.0 > end) {
@@ -622,17 +870,17 @@ void DecodeTrace::draw_range(const pv::data::decode::Annotation &a, QPainter &p,
 
        p.drawConvexPolygon(pts, countof(pts));
 
-       if (annotations.empty())
+       if (annotations->empty())
                return;
 
        const int ann_start = start + cap_width;
        const int ann_end = end - cap_width;
 
-       const int real_start = max(ann_start, pp.left() + row_title_width);
+       const int real_start = max(ann_start, pp.left() + ArrowSize + row_title_width);
        const int real_end = min(ann_end, pp.right());
        const int real_width = real_end - real_start;
 
-       QRectF rect(real_start, y - h / 2, real_width, h);
+       QRectF rect(real_start, y - annotation_height_ / 2, real_width, annotation_height_);
        if (rect.width() <= 4)
                return;
 
@@ -642,14 +890,14 @@ void DecodeTrace::draw_range(const pv::data::decode::Annotation &a, QPainter &p,
        QString best_annotation;
        int best_width = 0;
 
-       for (const QString &a : annotations) {
-               const int w = p.boundingRect(QRectF(), 0, a).width();
+       for (const QString &s : *annotations) {
+               const int w = p.boundingRect(QRectF(), 0, s).width();
                if (w <= rect.width() && w > best_width)
-                       best_annotation = a, best_width = w;
+                       best_annotation = s, best_width = w;
        }
 
        if (best_annotation.isEmpty())
-               best_annotation = annotations.back();
+               best_annotation = annotations->back();
 
        // If not ellide the last in the list
        p.drawText(rect, Qt::AlignCenter, p.fontMetrics().elidedText(
@@ -678,11 +926,8 @@ void DecodeTrace::draw_error(QPainter &p, const QString &message,
        p.drawText(text_rect, message);
 }
 
-void DecodeTrace::draw_unresolved_period(QPainter &p, int h, int left, int right) const
+void DecodeTrace::draw_unresolved_period(QPainter &p, int left, int right) const
 {
-       using namespace pv::data;
-       using pv::data::decode::Decoder;
-
        double samples_per_pixel, pixels_offset;
 
        const int64_t sample_count = decode_signal_->get_working_sample_count(current_segment_);
@@ -701,7 +946,8 @@ void DecodeTrace::draw_unresolved_period(QPainter &p, int h, int left, int right
                samples_per_pixel - pixels_offset, left - 1.0);
        const double end = min(sample_count / samples_per_pixel -
                pixels_offset, right + 1.0);
-       const QRectF no_decode_rect(start, y - (h / 2) - 0.5, end - start, h);
+       const QRectF no_decode_rect(start, y - (annotation_height_ / 2) - 0.5,
+               end - start, annotation_height_);
 
        p.setPen(QPen(Qt::NoPen));
        p.setBrush(Qt::white);
@@ -777,101 +1023,73 @@ QColor DecodeTrace::get_annotation_color(QColor row_color, int annotation_index)
        return color;
 }
 
-int DecodeTrace::get_row_at_point(const QPoint &point)
+unsigned int DecodeTrace::get_row_y(const DecodeTraceRow* row) const
 {
-       if (!row_height_)
-               return -1;
-
-       const int y = (point.y() - get_visual_y() + row_height_ / 2);
+       assert(row);
 
-       /* Integer divison of (x-1)/x would yield 0, so we check for this. */
-       if (y < 0)
-               return -1;
+       unsigned int y = get_visual_y();
 
-       const int row = y / row_height_;
+       for (const DecodeTraceRow& r : rows_) {
+               if (!r.currently_visible)
+                       continue;
 
-       if (row >= (int)visible_rows_.size())
-               return -1;
+               if (row->decode_row == r.decode_row)
+                       break;
+               else
+                       y += r.height;
+       }
 
-       return row;
+       return y;
 }
 
-const QString DecodeTrace::get_annotation_at_point(const QPoint &point)
+DecodeTraceRow* DecodeTrace::get_row_at_point(const QPoint &point)
 {
-       using namespace pv::data::decode;
-
-       if (!enabled())
-               return QString();
+       int y = get_visual_y() - (default_row_height_ / 2);
 
-       const pair<uint64_t, uint64_t> sample_range =
-               get_view_sample_range(point.x(), point.x() + 1);
-       const int row = get_row_at_point(point);
-       if (row < 0)
-               return QString();
+       for (DecodeTraceRow& r : rows_) {
+               if (!r.currently_visible)
+                       continue;
 
-       vector<Annotation> annotations;
+               if ((point.y() >= y) && (point.y() < (int)(y + r.height)))
+                       return &r;
 
-       decode_signal_->get_annotation_subset(annotations, visible_rows_[row],
-               current_segment_, sample_range.first, sample_range.second);
+               y += r.height;
+       }
 
-       return (annotations.empty()) ?
-               QString() : annotations[0].annotations().front();
+       return nullptr;
 }
 
-void DecodeTrace::hover_point_changed(const QPoint &hp)
+const QString DecodeTrace::get_annotation_at_point(const QPoint &point)
 {
-       Trace::hover_point_changed(hp);
-
-       assert(owner_);
-
-       const View *const view = owner_->view();
-       assert(view);
-
-       if (hp.x() == 0) {
-               QToolTip::hideText();
-               return;
-       }
-
-       QString ann = get_annotation_at_point(hp);
-
-       assert(view);
-
-       if (!row_height_ || ann.isEmpty()) {
-               QToolTip::hideText();
-               return;
-       }
+       if (!enabled())
+               return QString();
 
-       const int hover_row = get_row_at_point(hp);
+       const pair<uint64_t, uint64_t> sample_range =
+               get_view_sample_range(point.x(), point.x() + 1);
+       const DecodeTraceRow* r = get_row_at_point(point);
 
-       QFontMetrics m(QToolTip::font());
-       const QRect text_size = m.boundingRect(QRect(), 0, ann);
+       if (!r)
+               return QString();
 
-       // This is OS-specific and unfortunately we can't query it, so
-       // use an approximation to at least try to minimize the error.
-       const int padding = 8;
+       if (point.y() > (int)(get_row_y(r) + (annotation_height_ / 2)))
+               return QString();
 
-       // Make sure the tool tip doesn't overlap with the mouse cursor.
-       // If it did, the tool tip would constantly hide and re-appear.
-       // We also push it up by one row so that it appears above the
-       // decode trace, not below.
-       QPoint p = hp;
-       p.setX(hp.x() - (text_size.width() / 2) - padding);
+       deque<const Annotation*> annotations;
 
-       p.setY(get_visual_y() - (row_height_ / 2) +
-               (hover_row * row_height_) -
-               row_height_ - text_size.height() - padding);
+       decode_signal_->get_annotation_subset(annotations, r->decode_row,
+               current_segment_, sample_range.first, sample_range.second);
 
-       QToolTip::showText(view->viewport()->mapToGlobal(p), ann);
+       return (annotations.empty()) ?
+               QString() : annotations[0]->annotations()->front();
 }
 
-void DecodeTrace::create_decoder_form(int index,
-       shared_ptr<data::decode::Decoder> &dec, QWidget *parent,
-       QFormLayout *form)
+void DecodeTrace::create_decoder_form(int index, shared_ptr<Decoder> &dec,
+       QWidget *parent, QFormLayout *form)
 {
        GlobalSettings settings;
 
        assert(dec);
-       const srd_decoder *const decoder = dec->decoder();
+       const srd_decoder *const decoder = dec->get_srd_decoder();
        assert(decoder);
 
        const bool decoder_deletable = index > 0;
@@ -882,7 +1100,7 @@ void DecodeTrace::create_decoder_form(int index,
                        tr("%1:\n%2").arg(QString::fromUtf8(decoder->longname),
                                QString::fromUtf8(decoder->desc)),
                        nullptr, decoder_deletable);
-       group->set_decoder_visible(dec->shown());
+       group->set_decoder_visible(dec->visible());
 
        if (decoder_deletable) {
                delete_mapper_.setMapping(group, index);
@@ -987,10 +1205,8 @@ QComboBox* DecodeTrace::create_channel_selector_init_state(QWidget *parent,
        return selector;
 }
 
-void DecodeTrace::export_annotations(vector<Annotation> *annotations) const
+void DecodeTrace::export_annotations(deque<const Annotation*>& annotations) const
 {
-       using namespace pv::data::decode;
-
        GlobalSettings settings;
        const QString dir = settings.value("MainWindow/SaveDirectory").toString();
 
@@ -1004,30 +1220,54 @@ void DecodeTrace::export_annotations(vector<Annotation> *annotations) const
        const QString quote = format.contains("%q") ? "\"" : "";
        format = format.remove("%q");
 
+       const bool has_sample_range   = format.contains("%s");
+       const bool has_row_name       = format.contains("%r");
+       const bool has_dec_name       = format.contains("%d");
+       const bool has_class_name     = format.contains("%c");
+       const bool has_first_ann_text = format.contains("%1");
+       const bool has_all_ann_text   = format.contains("%a");
+
        QFile file(file_name);
        if (file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
                QTextStream out_stream(&file);
 
-               for (Annotation &ann : *annotations) {
-                       const QString sample_range = QString("%1-%2") \
-                               .arg(QString::number(ann.start_sample()), QString::number(ann.end_sample()));
+               for (const Annotation* ann : annotations) {
+                       QString out_text = format;
+
+                       if (has_sample_range) {
+                               const QString sample_range = QString("%1-%2") \
+                                       .arg(QString::number(ann->start_sample()), QString::number(ann->end_sample()));
+                               out_text = out_text.replace("%s", sample_range);
+                       }
 
-                       const QString class_name = quote + ann.row()->class_name() + quote;
+                       if (has_dec_name)
+                               out_text = out_text.replace("%d",
+                                       quote + QString::fromUtf8(ann->row()->decoder()->name()) + quote);
 
-                       QString all_ann_text;
-                       for (const QString &s : ann.annotations())
-                               all_ann_text = all_ann_text + quote + s + quote + ",";
-                       all_ann_text.chop(1);
+                       if (has_row_name) {
+                               const QString row_name = quote + ann->row()->description() + quote;
+                               out_text = out_text.replace("%r", row_name);
+                       }
 
-                       const QString first_ann_text = quote + ann.annotations().front() + quote;
+                       if (has_class_name) {
+                               const QString class_name = quote + ann->ann_class_name() + quote;
+                               out_text = out_text.replace("%c", class_name);
+                       }
+
+                       if (has_first_ann_text) {
+                               const QString first_ann_text = quote + ann->annotations()->front() + quote;
+                               out_text = out_text.replace("%1", first_ann_text);
+                       }
+
+                       if (has_all_ann_text) {
+                               QString all_ann_text;
+                               for (const QString &s : *(ann->annotations()))
+                                       all_ann_text = all_ann_text + quote + s + quote + ",";
+                               all_ann_text.chop(1);
+
+                               out_text = out_text.replace("%a", all_ann_text);
+                       }
 
-                       QString out_text = format;
-                       out_text = out_text.replace("%s", sample_range);
-                       out_text = out_text.replace("%d",
-                               quote + QString::fromUtf8(ann.row()->decoder()->name) + quote);
-                       out_text = out_text.replace("%c", class_name);
-                       out_text = out_text.replace("%1", first_ann_text);
-                       out_text = out_text.replace("%a", all_ann_text);
                        out_stream << out_text << '\n';
                }
 
@@ -1036,13 +1276,256 @@ void DecodeTrace::export_annotations(vector<Annotation> *annotations) const
        }
 
        QMessageBox msg(owner_->view());
-       msg.setText(tr("Error"));
-       msg.setInformativeText(tr("File %1 could not be written to.").arg(file_name));
+       msg.setText(tr("Error") + "\n\n" + tr("File %1 could not be written to.").arg(file_name));
        msg.setStandardButtons(QMessageBox::Ok);
        msg.setIcon(QMessageBox::Warning);
        msg.exec();
 }
 
+void DecodeTrace::initialize_row_widgets(DecodeTraceRow* r, unsigned int row_id)
+{
+       // Set colors and fixed widths
+       QFontMetrics m(QApplication::font());
+
+       QPalette header_palette = owner_->view()->palette();
+       QPalette selector_palette = owner_->view()->palette();
+
+       if (GlobalSettings::current_theme_is_dark()) {
+               header_palette.setColor(QPalette::Background,
+                       QColor(255, 255, 255, ExpansionAreaHeaderAlpha));
+               selector_palette.setColor(QPalette::Background,
+                       QColor(255, 255, 255, ExpansionAreaAlpha));
+       } else {
+               header_palette.setColor(QPalette::Background,
+                       QColor(0, 0, 0, ExpansionAreaHeaderAlpha));
+               selector_palette.setColor(QPalette::Background,
+                       QColor(0, 0, 0, ExpansionAreaAlpha));
+       }
+
+       const int w = m.boundingRect(r->decode_row->title()).width() + RowTitleMargin;
+       r->title_width = w;
+
+       // Set up top-level container
+       connect(r->container, SIGNAL(widgetResized(QWidget*)),
+               this, SLOT(on_row_container_resized(QWidget*)));
+
+       QVBoxLayout* vlayout = new QVBoxLayout();
+       r->container->setLayout(vlayout);
+
+       // Add header container
+       vlayout->addWidget(r->header_container);
+       vlayout->setContentsMargins(0, 0, 0, 0);
+       vlayout->setSpacing(0);
+       QHBoxLayout* header_container_layout = new QHBoxLayout();
+       r->header_container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
+       r->header_container->setMinimumSize(0, default_row_height_);
+       r->header_container->setLayout(header_container_layout);
+       r->header_container->layout()->setContentsMargins(10, 2, 10, 2);
+
+       r->header_container->setAutoFillBackground(true);
+       r->header_container->setPalette(header_palette);
+
+       // Add widgets inside the header container
+       QCheckBox* cb = new QCheckBox();
+       r->row_visibility_checkbox = cb;
+       header_container_layout->addWidget(cb);
+       cb->setText(tr("Show this row"));
+       cb->setChecked(r->decode_row->visible());
+
+       row_show_hide_mapper_.setMapping(cb, row_id);
+       connect(cb, SIGNAL(stateChanged(int)),
+               &row_show_hide_mapper_, SLOT(map()));
+
+       QPushButton* btn = new QPushButton();
+       header_container_layout->addWidget(btn);
+       btn->setFlat(true);
+       btn->setStyleSheet(":hover { background-color: palette(button); color: palette(button-text); border:0; }");
+       btn->setText(tr("Show All"));
+       btn->setProperty("decode_trace_row_ptr", QVariant::fromValue((void*)r));
+       connect(btn, SIGNAL(clicked(bool)), this, SLOT(on_show_all_classes()));
+
+       btn = new QPushButton();
+       header_container_layout->addWidget(btn);
+       btn->setFlat(true);
+       btn->setStyleSheet(":hover { background-color: palette(button); color: palette(button-text); border:0; }");
+       btn->setText(tr("Hide All"));
+       btn->setProperty("decode_trace_row_ptr", QVariant::fromValue((void*)r));
+       connect(btn, SIGNAL(clicked(bool)), this, SLOT(on_hide_all_classes()));
+
+       header_container_layout->addStretch(); // To left-align the header widgets
+
+       // Add selector container
+       vlayout->addWidget(r->selector_container);
+       r->selector_container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+       r->selector_container->setLayout(new FlowLayout(r->selector_container));
+
+       r->selector_container->setAutoFillBackground(true);
+       r->selector_container->setPalette(selector_palette);
+
+       // Add all classes that can be toggled
+       vector<AnnotationClass*> ann_classes = r->decode_row->ann_classes();
+
+       for (const AnnotationClass* ann_class : ann_classes) {
+               cb = new QCheckBox();
+               cb->setText(tr(ann_class->description));
+               cb->setChecked(ann_class->visible);
+
+               int dim = ViewItemPaintParams::text_height() - 2;
+               QPixmap pixmap(dim, dim);
+               pixmap.fill(r->ann_class_color[ann_class->id]);
+               cb->setIcon(pixmap);
+
+               r->selector_container->layout()->addWidget(cb);
+               r->selectors.push_back(cb);
+
+               cb->setProperty("ann_class_ptr", QVariant::fromValue((void*)ann_class));
+               cb->setProperty("decode_trace_row_ptr", QVariant::fromValue((void*)r));
+
+               class_show_hide_mapper_.setMapping(cb, cb);
+               connect(cb, SIGNAL(stateChanged(int)),
+                       &class_show_hide_mapper_, SLOT(map()));
+       }
+}
+
+void DecodeTrace::update_rows()
+{
+       if (!owner_)
+               return;
+
+       lock_guard<mutex> lock(row_modification_mutex_);
+
+       for (DecodeTraceRow& r : rows_)
+               r.exists = false;
+
+       unsigned int row_id = 0;
+       for (Row* decode_row : decode_signal_->get_rows()) {
+               // Find row in our list
+               auto r_it = find_if(rows_.begin(), rows_.end(),
+                       [&](DecodeTraceRow& r){ return r.decode_row == decode_row; });
+
+               DecodeTraceRow* r = nullptr;
+               if (r_it == rows_.end()) {
+                       // Row doesn't exist yet, create and append it
+                       DecodeTraceRow nr;
+                       nr.decode_row = decode_row;
+                       nr.height = default_row_height_;
+                       nr.expanded_height = default_row_height_;
+                       nr.currently_visible = false;
+                       nr.has_hidden_classes = decode_row->has_hidden_classes();
+                       nr.expand_marker_highlighted = false;
+                       nr.expanding = false;
+                       nr.expanded = false;
+                       nr.collapsing = false;
+                       nr.expand_marker_shape = default_marker_shape_;
+                       nr.container = new ContainerWidget(owner_->view()->scrollarea());
+                       nr.header_container = new QWidget(nr.container);
+                       nr.selector_container = new QWidget(nr.container);
+
+                       nr.row_color = get_row_color(decode_row->index());
+
+                       vector<AnnotationClass*> ann_classes = decode_row->ann_classes();
+                       for (const AnnotationClass* ann_class : ann_classes) {
+                               nr.ann_class_color[ann_class->id] =
+                                       get_annotation_color(nr.row_color, ann_class->id);
+                               nr.ann_class_dark_color[ann_class->id] =
+                                       nr.ann_class_color[ann_class->id].darker();
+                       }
+
+                       rows_.push_back(nr);
+                       r = &rows_.back();
+                       initialize_row_widgets(r, row_id);
+               } else
+                       r = &(*r_it);
+
+               r->exists = true;
+               row_id++;
+       }
+
+       // If there's only one row, it must not be hidden or else it can't be un-hidden
+       if (row_id == 1)
+               rows_.front().row_visibility_checkbox->setEnabled(false);
+
+       // Remove any rows that no longer exist, obeying that iterators are invalidated
+       bool any_exists;
+       do {
+               any_exists = false;
+
+               for (unsigned int i = 0; i < rows_.size(); i++)
+                       if (!rows_[i].exists) {
+                               delete rows_[i].row_visibility_checkbox;
+
+                               for (QCheckBox* cb : rows_[i].selectors)
+                                       delete cb;
+
+                               delete rows_[i].selector_container;
+                               delete rows_[i].header_container;
+                               delete rows_[i].container;
+
+                               rows_.erase(rows_.begin() + i);
+                               any_exists = true;
+                               break;
+                       }
+       } while (any_exists);
+}
+
+void DecodeTrace::set_row_expanded(DecodeTraceRow* r)
+{
+       r->height = r->expanded_height;
+       r->expanding = false;
+       r->expanded = true;
+
+       // For details on this, see on_animation_timer()
+       r->expand_marker_shape.setPoint(0, 0, 0);
+       r->expand_marker_shape.setPoint(1, ArrowSize, ArrowSize);
+       r->expand_marker_shape.setPoint(2, 2*ArrowSize, 0);
+
+       r->container->resize(owner_->view()->viewport()->width() - r->container->pos().x(),
+               r->height - 2 * default_row_height_);
+}
+
+void DecodeTrace::set_row_collapsed(DecodeTraceRow* r)
+{
+       r->height = default_row_height_;
+       r->collapsing = false;
+       r->expanded = false;
+       r->expand_marker_shape = default_marker_shape_;
+       r->container->setVisible(false);
+
+       r->container->resize(owner_->view()->viewport()->width() - r->container->pos().x(),
+               r->height - 2 * default_row_height_);
+}
+
+void DecodeTrace::update_expanded_rows()
+{
+       for (DecodeTraceRow& r : rows_) {
+               if (r.expanding || r.expanded)
+                       r.expanded_height = 2 * default_row_height_ + r.container->sizeHint().height();
+
+               if (r.expanded)
+                       r.height = r.expanded_height;
+
+               int x = 2 * ArrowSize;
+               int y = get_row_y(&r) + default_row_height_;
+               // Only update the position if it actually changes
+               if ((x != r.container->pos().x()) || (y != r.container->pos().y()))
+                       r.container->move(x, y);
+
+               int w = owner_->view()->viewport()->width() - x;
+               int h = r.height - 2 * default_row_height_;
+               // Only update the dimension if they actually change
+               if ((w != r.container->sizeHint().width()) || (h != r.container->sizeHint().height()))
+                       r.container->resize(w, h);
+       }
+}
+
+void DecodeTrace::on_setting_changed(const QString &key, const QVariant &value)
+{
+       Trace::on_setting_changed(key, value);
+
+       if (key == GlobalSettings::Key_Dec_AlwaysShowAllRows)
+               always_show_all_rows_ = value.toBool();
+}
+
 void DecodeTrace::on_new_annotations()
 {
        if (!delayed_trace_updater_.isActive())
@@ -1057,8 +1540,7 @@ void DecodeTrace::on_delayed_trace_update()
 
 void DecodeTrace::on_decode_reset()
 {
-       visible_rows_.clear();
-       max_visible_rows_ = 0;
+       update_rows();
 
        if (owner_)
                owner_->row_item_appearance_changed(false, true);
@@ -1078,11 +1560,6 @@ void DecodeTrace::on_pause_decode()
                decode_signal_->pause_decode();
 }
 
-void DecodeTrace::delete_pressed()
-{
-       on_delete();
-}
-
 void DecodeTrace::on_delete()
 {
        session_.remove_decode_signal(decode_signal_);
@@ -1124,6 +1601,7 @@ void DecodeTrace::on_init_state_changed(int)
 void DecodeTrace::on_stack_decoder(srd_decoder *decoder)
 {
        decode_signal_->stack_decoder(decoder);
+       update_rows();
 
        create_popup_form();
 }
@@ -1131,12 +1609,10 @@ void DecodeTrace::on_stack_decoder(srd_decoder *decoder)
 void DecodeTrace::on_delete_decoder(int index)
 {
        decode_signal_->remove_decoder(index);
+       update_rows();
 
-       // Force re-calculation of the trace height, see paint_mid()
-       max_visible_rows_ = 0;
        owner_->extents_changed(false, true);
 
-       // Update the popup
        create_popup_form();
 }
 
@@ -1147,14 +1623,97 @@ void DecodeTrace::on_show_hide_decoder(int index)
        assert(index < (int)decoder_forms_.size());
        decoder_forms_[index]->set_decoder_visible(state);
 
-       if (!state) {
-               // Force re-calculation of the trace height, see paint_mid()
-               max_visible_rows_ = 0;
+       if (!state)
                owner_->extents_changed(false, true);
-       }
 
-       if (owner_)
-               owner_->row_item_appearance_changed(false, true);
+       owner_->row_item_appearance_changed(false, true);
+}
+
+void DecodeTrace::on_show_hide_row(int row_id)
+{
+       if (row_id >= (int)rows_.size())
+               return;
+
+       rows_[row_id].decode_row->set_visible(!rows_[row_id].decode_row->visible());
+
+       if (!rows_[row_id].decode_row->visible())
+               set_row_collapsed(&rows_[row_id]);
+
+       owner_->extents_changed(false, true);
+       owner_->row_item_appearance_changed(false, true);
+}
+
+void DecodeTrace::on_show_hide_class(QWidget* sender)
+{
+       void* ann_class_ptr = sender->property("ann_class_ptr").value<void*>();
+       assert(ann_class_ptr);
+       AnnotationClass* ann_class = (AnnotationClass*)ann_class_ptr;
+
+       ann_class->visible = !ann_class->visible;
+
+       void* row_ptr = sender->property("decode_trace_row_ptr").value<void*>();
+       assert(row_ptr);
+       DecodeTraceRow* row = (DecodeTraceRow*)row_ptr;
+
+       row->has_hidden_classes = row->decode_row->has_hidden_classes();
+
+       owner_->row_item_appearance_changed(false, true);
+}
+
+void DecodeTrace::on_show_all_classes()
+{
+       void* row_ptr = QObject::sender()->property("decode_trace_row_ptr").value<void*>();
+       assert(row_ptr);
+       DecodeTraceRow* row = (DecodeTraceRow*)row_ptr;
+
+       for (QCheckBox* cb : row->selectors)
+               cb->setChecked(true);
+
+       row->has_hidden_classes = false;
+
+       owner_->row_item_appearance_changed(false, true);
+}
+
+void DecodeTrace::on_hide_all_classes()
+{
+       void* row_ptr = QObject::sender()->property("decode_trace_row_ptr").value<void*>();
+       assert(row_ptr);
+       DecodeTraceRow* row = (DecodeTraceRow*)row_ptr;
+
+       for (QCheckBox* cb : row->selectors)
+               cb->setChecked(false);
+
+       row->has_hidden_classes = true;
+
+       owner_->row_item_appearance_changed(false, true);
+}
+
+void DecodeTrace::on_row_container_resized(QWidget* sender)
+{
+       sender->update();
+
+       owner_->extents_changed(false, true);
+       owner_->row_item_appearance_changed(false, true);
+}
+
+void DecodeTrace::on_copy_annotation_to_clipboard()
+{
+       if (!selected_row_)
+               return;
+
+       deque<const Annotation*> annotations;
+
+       decode_signal_->get_annotation_subset(annotations, selected_row_,
+               current_segment_, selected_sample_range_.first, selected_sample_range_.first);
+
+       if (annotations.empty())
+               return;
+
+       QClipboard *clipboard = QApplication::clipboard();
+       clipboard->setText(annotations.front()->annotations()->front(), QClipboard::Clipboard);
+
+       if (clipboard->supportsSelection())
+               clipboard->setText(annotations.front()->annotations()->front(), QClipboard::Selection);
 }
 
 void DecodeTrace::on_export_row()
@@ -1223,36 +1782,92 @@ void DecodeTrace::on_export_all_rows_with_cursor()
 
 void DecodeTrace::on_export_row_from_here()
 {
-       using namespace pv::data::decode;
-
        if (!selected_row_)
                return;
 
-       vector<Annotation> *annotations = new vector<Annotation>();
+       deque<const Annotation*> annotations;
 
-       decode_signal_->get_annotation_subset(*annotations, *selected_row_,
+       decode_signal_->get_annotation_subset(annotations, selected_row_,
                current_segment_, selected_sample_range_.first, selected_sample_range_.second);
 
-       if (annotations->empty())
+       if (annotations.empty())
                return;
 
        export_annotations(annotations);
-       delete annotations;
 }
 
 void DecodeTrace::on_export_all_rows_from_here()
 {
-       using namespace pv::data::decode;
-
-       vector<Annotation> *annotations = new vector<Annotation>();
+       deque<const Annotation*> annotations;
 
-       decode_signal_->get_annotation_subset(*annotations, current_segment_,
+       decode_signal_->get_annotation_subset(annotations, current_segment_,
                        selected_sample_range_.first, selected_sample_range_.second);
 
-       if (!annotations->empty())
+       if (!annotations.empty())
                export_annotations(annotations);
+}
+
+void DecodeTrace::on_animation_timer()
+{
+       bool animation_finished = true;
+
+       for (DecodeTraceRow& r : rows_) {
+               if (!(r.expanding || r.collapsing))
+                       continue;
+
+               unsigned int height_delta = r.expanded_height - default_row_height_;
+
+               if (r.expanding) {
+                       if (r.height < r.expanded_height) {
+                               r.anim_height += height_delta / (float)AnimationDurationInTicks;
+                               r.height = min((int)r.anim_height, (int)r.expanded_height);
+                               r.anim_shape += ArrowSize / (float)AnimationDurationInTicks;
+                               animation_finished = false;
+                       } else
+                               set_row_expanded(&r);
+               }
+
+               if (r.collapsing) {
+                       if (r.height > default_row_height_) {
+                               r.anim_height -= height_delta / (float)AnimationDurationInTicks;
+                               r.height = max((int)r.anim_height, (int)0);
+                               r.anim_shape -= ArrowSize / (float)AnimationDurationInTicks;
+                               animation_finished = false;
+                       } else
+                               set_row_collapsed(&r);
+               }
 
-       delete annotations;
+               // The expansion marker shape switches between
+               // 0/-A, A/0,  0/A (default state; anim_shape=0) and
+               // 0/ 0, A/A, 2A/0 (expanded state; anim_shape=ArrowSize)
+
+               r.expand_marker_shape.setPoint(0, 0, -ArrowSize + r.anim_shape);
+               r.expand_marker_shape.setPoint(1, ArrowSize, r.anim_shape);
+               r.expand_marker_shape.setPoint(2, 2*r.anim_shape, ArrowSize - r.anim_shape);
+       }
+
+       if (animation_finished)
+               animation_timer_.stop();
+
+       owner_->extents_changed(false, true);
+       owner_->row_item_appearance_changed(false, true);
+}
+
+void DecodeTrace::on_hide_hidden_rows()
+{
+       // Make all hidden traces invisible again unless the user is hovering over a row name
+       bool any_highlighted = false;
+
+       for (DecodeTraceRow& r : rows_)
+               if (r.expand_marker_highlighted)
+                       any_highlighted = true;
+
+       if (!any_highlighted) {
+               show_hidden_rows_ = false;
+
+               owner_->extents_changed(false, true);
+               owner_->row_item_appearance_changed(false, true);
+       }
 }
 
 } // namespace trace
index 3d25c3e4d7410f10853204ba21116dc363610c40..258509e66ea4ec4869bda2a185e95454f73130d7 100644 (file)
@@ -20,6 +20,7 @@
 #ifndef PULSEVIEW_PV_VIEWS_TRACEVIEW_DECODETRACE_HPP
 #define PULSEVIEW_PV_VIEWS_TRACEVIEW_DECODETRACE_HPP
 
+#include <config.h>
 #include "trace.hpp"
 
 #include <list>
 
 #include <QColor>
 #include <QComboBox>
+#include <QCheckBox>
+#include <QElapsedTimer>
+#include <QPolygon>
+#include <QPushButton>
 #include <QSignalMapper>
 #include <QTimer>
 
 #include <pv/binding/decoder.hpp>
+#include <pv/data/decode/decoder.hpp>
 #include <pv/data/decode/annotation.hpp>
 #include <pv/data/decode/row.hpp>
 #include <pv/data/signalbase.hpp>
 
+#define DECODETRACE_SHOW_RENDER_TIME 0
+
+using std::deque;
 using std::list;
 using std::map;
+using std::mutex;
 using std::pair;
 using std::shared_ptr;
 using std::vector;
 
+using pv::data::SignalBase;
+using pv::data::decode::Annotation;
+using pv::data::decode::Decoder;
+using pv::data::decode::Row;
+
 struct srd_channel;
 struct srd_decoder;
 
@@ -56,6 +71,7 @@ class DecodeSignal;
 
 namespace decode {
 class Decoder;
+class Row;
 }
 }  // namespace data
 
@@ -66,6 +82,43 @@ class DecoderGroupBox;
 namespace views {
 namespace trace {
 
+class ContainerWidget;
+
+struct DecodeTraceRow {
+       // When adding a field, make sure it's initialized properly in
+       // DecodeTrace::update_rows()
+
+       Row* decode_row;
+       unsigned int height, expanded_height, title_width, animation_step;
+       bool exists, currently_visible, has_hidden_classes;
+       bool expand_marker_highlighted, expanding, expanded, collapsing;
+       QPolygon expand_marker_shape;
+       float anim_height, anim_shape;
+
+       ContainerWidget* container;
+       QWidget* header_container;
+       QWidget* selector_container;
+       QCheckBox* row_visibility_checkbox;
+       vector<QCheckBox*> selectors;
+
+       QColor row_color;
+       map<uint32_t, QColor> ann_class_color;
+       map<uint32_t, QColor> ann_class_dark_color;
+};
+
+class ContainerWidget : public QWidget
+{
+       Q_OBJECT
+
+public:
+       ContainerWidget(QWidget *parent = nullptr);
+
+       virtual void resizeEvent(QResizeEvent* event);
+
+Q_SIGNALS:
+       void widgetResized(QWidget* sender);
+};
+
 class DecodeTrace : public Trace
 {
        Q_OBJECT
@@ -73,6 +126,10 @@ class DecodeTrace : public Trace
 private:
        static const QColor ErrorBgColor;
        static const QColor NoDecodeColor;
+       static const QColor ExpandMarkerWarnColor;
+       static const QColor ExpandMarkerHiddenColor;
+       static const uint8_t ExpansionAreaHeaderAlpha;
+       static const uint8_t ExpansionAreaAlpha;
 
        static const int ArrowSize;
        static const double EndCapWidth;
@@ -80,14 +137,24 @@ private:
        static const int DrawPadding;
 
        static const int MaxTraceUpdateRate;
+       static const int AnimationDurationInTicks;
+       static const int HiddenRowHideDelay;
 
 public:
-       DecodeTrace(pv::Session &session, shared_ptr<data::SignalBase> signalbase,
+       DecodeTrace(pv::Session &session, shared_ptr<SignalBase> signalbase,
                int index);
 
+       ~DecodeTrace();
+
        bool enabled() const;
 
-       shared_ptr<data::SignalBase> base() const;
+       shared_ptr<SignalBase> base() const;
+
+       /**
+        * Sets the owner this trace in the view trace hierachy.
+        * @param The new owner of the trace.
+        */
+       virtual void set_owner(TraceTreeItemOwner *owner);
 
        /**
         * Computes the vertical extents of the contents of this row item.
@@ -122,33 +189,30 @@ public:
 
        virtual QMenu* create_view_context_menu(QWidget *parent, QPoint &click_pos);
 
-       void delete_pressed();
+       virtual void delete_pressed();
+
+       virtual void hover_point_changed(const QPoint &hp);
+
+       virtual void mouse_left_press_event(const QMouseEvent* event);
 
 private:
-       void draw_annotations(vector<pv::data::decode::Annotation> annotations,
-               QPainter &p, int h, const ViewItemPaintParams &pp, int y,
-               QColor row_color, int row_title_width);
+       void draw_annotations(deque<const Annotation*>& annotations, QPainter &p,
+               const ViewItemPaintParams &pp, int y, const DecodeTraceRow& row);
 
-       void draw_annotation(const pv::data::decode::Annotation &a, QPainter &p,
-               int h, const ViewItemPaintParams &pp, int y,
-               QColor row_color, int row_title_width) const;
+       void draw_annotation(const Annotation* a, QPainter &p,
+               const ViewItemPaintParams &pp, int y, const DecodeTraceRow& row) const;
 
-       void draw_annotation_block(qreal start, qreal end,
-               pv::data::decode::Annotation::Class ann_class, bool use_ann_format,
-               QPainter &p, int h, int y, QColor row_color) const;
+       void draw_annotation_block(qreal start, qreal end, Annotation::Class ann_class,
+               bool use_ann_format, QPainter &p, int y, const DecodeTraceRow& row) const;
 
-       void draw_instant(const pv::data::decode::Annotation &a, QPainter &p,
-               int h, qreal x, int y) const;
+       void draw_instant(const Annotation* a, QPainter &p, qreal x, int y) const;
 
-       void draw_range(const pv::data::decode::Annotation &a, QPainter &p,
-               int h, qreal start, qreal end, int y, const ViewItemPaintParams &pp,
-               int row_title_width) const;
+       void draw_range(const Annotation* a, QPainter &p, qreal start, qreal end,
+               int y, const ViewItemPaintParams &pp, int row_title_width) const;
 
-       void draw_error(QPainter &p, const QString &message,
-               const ViewItemPaintParams &pp);
+       void draw_error(QPainter &p, const QString &message, const ViewItemPaintParams &pp);
 
-       void draw_unresolved_period(QPainter &p, int h, int left,
-               int right) const;
+       void draw_unresolved_period(QPainter &p, int left, int right) const;
 
        pair<double, double> get_pixels_offset_samples_per_pixel() const;
 
@@ -164,25 +228,42 @@ private:
        QColor get_row_color(int row_index) const;
        QColor get_annotation_color(QColor row_color, int annotation_index) const;
 
-       int get_row_at_point(const QPoint &point);
+       unsigned int get_row_y(const DecodeTraceRow* row) const;
+
+       DecodeTraceRow* get_row_at_point(const QPoint &point);
 
        const QString get_annotation_at_point(const QPoint &point);
 
-       void create_decoder_form(int index,
-               shared_ptr<pv::data::decode::Decoder> &dec,
+       void update_stack_button();
+
+       void create_decoder_form(int index, shared_ptr<Decoder> &dec,
                QWidget *parent, QFormLayout *form);
 
        QComboBox* create_channel_selector(QWidget *parent,
-               const data::DecodeChannel *ch);
+               const data::decode::DecodeChannel *ch);
        QComboBox* create_channel_selector_init_state(QWidget *parent,
-               const data::DecodeChannel *ch);
+               const data::decode::DecodeChannel *ch);
 
-       void export_annotations(vector<data::decode::Annotation> *annotations) const;
+       void export_annotations(deque<const Annotation*>& annotations) const;
 
-public:
-       virtual void hover_point_changed(const QPoint &hp);
+       void initialize_row_widgets(DecodeTraceRow* r, unsigned int row_id);
+       void update_rows();
+
+       /**
+        * Sets row r to expanded state without forcing an update of the view
+        */
+       void set_row_expanded(DecodeTraceRow* r);
+
+       /**
+        * Sets row r to collapsed state without forcing an update of the view
+        */
+       void set_row_collapsed(DecodeTraceRow* r);
+
+       void update_expanded_rows();
 
 private Q_SLOTS:
+       void on_setting_changed(const QString &key, const QVariant &value);
+
        void on_new_annotations();
        void on_delayed_trace_update();
        void on_decode_reset();
@@ -202,6 +283,13 @@ private Q_SLOTS:
        void on_delete_decoder(int index);
 
        void on_show_hide_decoder(int index);
+       void on_show_hide_row(int row_id);
+       void on_show_hide_class(QWidget* sender);
+       void on_show_all_classes();
+       void on_hide_all_classes();
+       void on_row_container_resized(QWidget* sender);
+
+       void on_copy_annotation_to_clipboard();
 
        void on_export_row();
        void on_export_all_rows();
@@ -210,29 +298,42 @@ private Q_SLOTS:
        void on_export_row_from_here();
        void on_export_all_rows_from_here();
 
+       void on_animation_timer();
+       void on_hide_hidden_rows();
+
 private:
        pv::Session &session_;
        shared_ptr<data::DecodeSignal> decode_signal_;
 
-       vector<data::decode::Row> visible_rows_;
+       deque<DecodeTraceRow> rows_;
+       mutable mutex row_modification_mutex_;
 
        map<QComboBox*, uint16_t> channel_id_map_;  // channel selector -> decode channel ID
        map<QComboBox*, uint16_t> init_state_map_;  // init state selector -> decode channel ID
        list< shared_ptr<pv::binding::Decoder> > bindings_;
 
-       data::decode::Row *selected_row_;
+       const Row* selected_row_;
        pair<uint64_t, uint64_t> selected_sample_range_;
 
        vector<pv::widgets::DecoderGroupBox*> decoder_forms_;
+       QPushButton* stack_button_;
 
-       map<data::decode::Row, int> row_title_widths_;
-       int row_height_, max_visible_rows_;
+       unsigned int default_row_height_, annotation_height_;
+       unsigned int visible_rows_;
 
        int min_useful_label_width_;
+       bool always_show_all_rows_, show_hidden_rows_;
 
        QSignalMapper delete_mapper_, show_hide_mapper_;
+       QSignalMapper row_show_hide_mapper_, class_show_hide_mapper_;
+
+       QTimer delayed_trace_updater_, animation_timer_, delayed_hidden_row_hider_;
+
+       QPolygon default_marker_shape_;
 
-       QTimer delayed_trace_updater_;
+#if DECODETRACE_SHOW_RENDER_TIME
+       QElapsedTimer render_time_;
+#endif
 };
 
 } // namespace trace
index 64ef1047d03e6b5057753ccca7fe0a468aefee44..b0518b646d87c493d8c13de606570f8dccf92522 100644 (file)
 
 #include "timemarker.hpp"
 #include "view.hpp"
+#include "ruler.hpp"
 
 #include <QColor>
 #include <QFormLayout>
 #include <QLineEdit>
 #include <QMenu>
+#include <QApplication>
 
 #include <libsigrokcxx/libsigrokcxx.hpp>
 
@@ -57,7 +59,58 @@ bool Flag::enabled() const
 
 QString Flag::get_text() const
 {
-       return text_;
+       QString s;
+
+       const shared_ptr<TimeItem> ref_item = view_.ruler()->get_reference_item();
+
+       if (!ref_item || (ref_item.get() == this))
+               s = text_;
+       else
+               s = Ruler::format_time_with_distance(
+                       ref_item->time(), ref_item->delta(time_),
+                       view_.tick_prefix(), view_.time_unit(), view_.tick_precision());
+
+       return s;
+}
+
+void Flag::set_text(const QString &text)
+{
+       text_ = text;
+       view_.time_item_appearance_changed(true, false);
+}
+
+QRectF Flag::label_rect(const QRectF &rect) const
+{
+       QRectF r;
+
+       const shared_ptr<TimeItem> ref_item = view_.ruler()->get_reference_item();
+
+       if (!ref_item || (ref_item.get() == this)) {
+               r = TimeMarker::label_rect(rect);
+       } else {
+               // TODO: Remove code duplication between here and cursor.cpp
+               const float x = get_x();
+
+               QFontMetrics m(QApplication::font());
+               QSize text_size = m.boundingRect(get_text()).size();
+
+               const QSizeF label_size(
+                       text_size.width() + LabelPadding.width() * 2,
+                       text_size.height() + LabelPadding.height() * 2);
+
+               const float height = label_size.height();
+               const float top =
+                       rect.height() - label_size.height() - TimeMarker::ArrowSize - 0.5f;
+
+               const pv::util::Timestamp& delta = ref_item->delta(time_);
+
+               if (delta >= 0)
+                       r = QRectF(x, top, label_size.width(), height);
+               else
+                       r = QRectF(x - label_size.width(), top, label_size.width(), height);
+       }
+
+       return r;
 }
 
 pv::widgets::Popup* Flag::create_popup(QWidget *parent)
@@ -90,6 +143,12 @@ QMenu* Flag::create_header_context_menu(QWidget *parent)
        connect(del, SIGNAL(triggered()), this, SLOT(on_delete()));
        menu->addAction(del);
 
+       QAction *const snap_disable = new QAction(tr("Disable snapping"), this);
+       snap_disable->setCheckable(true);
+       snap_disable->setChecked(snapping_disabled_);
+       connect(snap_disable, &QAction::toggled, this, [=](bool checked){snapping_disabled_ = checked;});
+       menu->addAction(snap_disable);
+
        return menu;
 }
 
@@ -105,8 +164,7 @@ void Flag::on_delete()
 
 void Flag::on_text_changed(const QString &text)
 {
-       text_ = text;
-       view_.time_item_appearance_changed(true, false);
+       set_text(text);
 }
 
 } // namespace trace
index 4bf6ebd5db7f974a9857e733ce0b49c8cbde046f..e58771b81a4f4f603d8171fdfb8905d8a6edb6e1 100644 (file)
@@ -60,18 +60,25 @@ public:
        /**
         * Returns true if the item is visible and enabled.
         */
-       bool enabled() const;
+       virtual bool enabled() const override;
 
        /**
         * Gets the text to show in the marker.
         */
-       QString get_text() const;
+       virtual QString get_text() const override;
 
-       pv::widgets::Popup* create_popup(QWidget *parent);
+       /**
+        * Sets the text to show in the marker.
+        */
+       virtual void set_text(const QString &text) override;
+
+       virtual pv::widgets::Popup* create_popup(QWidget *parent) override;
+
+       virtual QMenu* create_header_context_menu(QWidget *parent) override;
 
-       QMenu* create_header_context_menu(QWidget *parent);
+       virtual void delete_pressed() override;
 
-       void delete_pressed();
+       QRectF label_rect(const QRectF &rect) const override;
 
 private Q_SLOTS:
        void on_delete();
index d7da7e03706bdab45389f127c8bc874871a3c74a..1e97521afd7d7b41481eb1b259fc5e8df3e770bf 100644 (file)
@@ -99,16 +99,16 @@ void Header::paintEvent(QPaintEvent*)
 {
        const QRect rect(0, 0, width(), height());
 
-       vector< shared_ptr<RowItem> > items(view_.list_by_type<RowItem>());
+       vector< shared_ptr<ViewItem> > items(view_.list_by_type<ViewItem>());
 
        stable_sort(items.begin(), items.end(),
-               [](const shared_ptr<RowItem> &a, const shared_ptr<RowItem> &b) {
+               [](const shared_ptr<ViewItem> &a, const shared_ptr<ViewItem> &b) {
                        return a->drag_point(QRect()).y() < b->drag_point(QRect()).y(); });
 
        QPainter painter(this);
        painter.setRenderHint(QPainter::Antialiasing);
 
-       for (const shared_ptr<RowItem>& r : items) {
+       for (const shared_ptr<ViewItem>& r : items) {
                assert(r);
 
                const bool highlight = !item_dragging_ &&
index f9ab16f102cdf5328613d81957a5d4563602d07e..a3fd7a0bbdcf9dc18c3561673497414c65a3b1b5 100644 (file)
@@ -146,16 +146,21 @@ shared_ptr<pv::data::Logic> LogicSignal::logic_data() const
        return base_->logic_data();
 }
 
-void LogicSignal::save_settings(QSettings &settings) const
+std::map<QString, QVariant> LogicSignal::save_settings() const
 {
-       settings.setValue("trace_height", signal_height_);
+       std::map<QString, QVariant> result;
+
+       result["trace_height"] = signal_height_;
+
+       return result;
 }
 
-void LogicSignal::restore_settings(QSettings &settings)
+void LogicSignal::restore_settings(std::map<QString, QVariant> settings)
 {
-       if (settings.contains("trace_height")) {
+       auto entry = settings.find("trace_height");
+       if (entry != settings.end()) {
                const int old_height = signal_height_;
-               signal_height_ = settings.value("trace_height").toInt();
+               signal_height_ = settings["trace_height"].toInt();
 
                if ((signal_height_ != old_height) && owner_) {
                        // Call order is important, otherwise the lazy event handler won't work
index b170e2c08d76da930f5b6d482fad9ad1ce4cb6d4..b769ec55914cfea4af1d6a685e3a53a96528fe96 100644 (file)
@@ -81,8 +81,8 @@ public:
 
        shared_ptr<pv::data::Logic> logic_data() const;
 
-       virtual void save_settings(QSettings &settings) const;
-       virtual void restore_settings(QSettings &settings);
+       virtual std::map<QString, QVariant> save_settings() const;
+       virtual void restore_settings(std::map<QString, QVariant> settings);
 
        /**
         * Computes the vertical extents of the contents of this row item.
index 86ec069bc2370147b76b9cb16bfc606655f2cabf..537ffe5ef298ad29b1cc6dd00a9ab5bab1992fd5 100644 (file)
@@ -47,6 +47,9 @@ void MarginWidget::item_clicked(const shared_ptr<ViewItem> &item)
 void MarginWidget::show_popup(const shared_ptr<ViewItem> &item)
 {
        pv::widgets::Popup *const p = item->create_popup(this);
+
+       connect(p, SIGNAL(closed()), this, SLOT(on_popup_closed()));
+
        if (p)
                p->show();
 }
@@ -76,8 +79,21 @@ void MarginWidget::keyPressEvent(QKeyEvent *event)
                        if (i->selected())
                                i->delete_pressed();
        }
+
+    ViewWidget::keyPressEvent(event);
 }
 
+void MarginWidget::on_popup_closed()
+{
+       bool cursor_above_widget = rect().contains(mapFromGlobal(QCursor::pos()));
+
+       if (!cursor_above_widget)
+               mouse_point_ = QPoint(INT_MIN, INT_MIN);
+
+       update();
+}
+
+
 } // namespace trace
 } // namespace views
 } // namespace pv
index 9a0686aa4c93e1ffd4448411ee2b035c7330a742..ef5e6350fe1cd1e4b1f6b9ba0ddfd01e4dc7ce6f 100644 (file)
@@ -61,10 +61,12 @@ protected:
         */
        void show_popup(const shared_ptr<ViewItem> &item);
 
-protected:
+protected Q_SLOTS:
        virtual void contextMenuEvent(QContextMenuEvent *event);
 
        virtual void keyPressEvent(QKeyEvent *event);
+
+       virtual void on_popup_closed();
 };
 
 } // namespace trace
diff --git a/pv/views/trace/rowitem.cpp b/pv/views/trace/rowitem.cpp
deleted file mode 100644 (file)
index c57043d..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * This file is part of the PulseView project.
- *
- * Copyright (C) 2015 Joel Holdsworth <joel@airwebreathe.org.uk>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, see <http://www.gnu.org/licenses/>.
- */
-
-#include "rowitem.hpp"
-
-namespace pv {
-namespace views {
-namespace trace {
-
-void RowItem::hover_point_changed(const QPoint &hp)
-{
-       (void)hp;
-}
-
-} // namespace trace
-} // namespace views
-} // namespace pv
diff --git a/pv/views/trace/rowitem.hpp b/pv/views/trace/rowitem.hpp
deleted file mode 100644 (file)
index d2a205f..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * This file is part of the PulseView project.
- *
- * Copyright (C) 2013 Joel Holdsworth <joel@airwebreathe.org.uk>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, see <http://www.gnu.org/licenses/>.
- */
-
-#ifndef PULSEVIEW_PV_VIEWS_TRACEVIEW_ROWITEM_HPP
-#define PULSEVIEW_PV_VIEWS_TRACEVIEW_ROWITEM_HPP
-
-#include "viewitem.hpp"
-
-namespace pv {
-namespace views {
-namespace trace {
-
-class RowItem : public ViewItem
-{
-       Q_OBJECT
-
-public:
-       virtual void hover_point_changed(const QPoint &hp);
-};
-
-} // namespace trace
-} // namespace views
-} // namespace pv
-
-#endif // PULSEVIEW_PV_VIEWS_TRACEVIEW_ROWITEM_HPP
index acea8a368b457469e44c11e758f95f935a56e68e..555794fc42c6e881c5691fc1d64c43a0355e7cd3 100644 (file)
@@ -19,7 +19,6 @@
 
 #include <extdef.h>
 
-#include <QApplication>
 #include <QFontMetrics>
 #include <QMenu>
 #include <QMouseEvent>
@@ -99,6 +98,11 @@ QString Ruler::format_time_with_distance(
        if (unit == pv::util::TimeUnit::Samples)
                return pv::util::format_time_si_adjusted(t, prefix, precision, "sa", sign);
 
+       QString unit_string;
+       if (unit == pv::util::TimeUnit::Time)
+               unit_string = "s";
+       // Note: In case of pv::util::TimeUnit::None, unit_string remains empty
+
        // View zoomed way out -> low precision (0), big distance (>=60s)
        // -> DD:HH:MM
        if ((precision == 0) && (distance >= limit))
@@ -109,16 +113,31 @@ QString Ruler::format_time_with_distance(
        // View zoomed way in -> high precision (>3), low step size (<1s)
        // -> HH:MM:SS.mmm... or xxxx (si unit) if less than limit seconds
        if (abs(t) < limit)
-               return pv::util::format_time_si_adjusted(t, prefix, precision, "s", sign);
+               return pv::util::format_time_si_adjusted(t, prefix, precision, unit_string, sign);
        else
                return pv::util::format_time_minutes(t, precision, sign);
 }
 
-pv::util::Timestamp Ruler::get_time_from_x_pos(uint32_t x) const
+pv::util::Timestamp Ruler::get_absolute_time_from_x_pos(uint32_t x) const
+{
+       return view_.offset() + ((double)x + 0.5) * view_.scale();
+}
+
+pv::util::Timestamp Ruler::get_ruler_time_from_x_pos(uint32_t x) const
 {
        return view_.ruler_offset() + ((double)x + 0.5) * view_.scale();
 }
 
+pv::util::Timestamp Ruler::get_ruler_time_from_absolute_time(const pv::util::Timestamp& abs_time) const
+{
+       return abs_time + view_.zero_offset();
+}
+
+pv::util::Timestamp Ruler::get_absolute_time_from_ruler_time(const pv::util::Timestamp& ruler_time) const
+{
+       return ruler_time - view_.zero_offset();
+}
+
 void Ruler::contextMenuEvent(QContextMenuEvent *event)
 {
        MarginWidget::contextMenuEvent(event);
@@ -139,6 +158,12 @@ void Ruler::contextMenuEvent(QContextMenuEvent *event)
        connect(set_zero_position, SIGNAL(triggered()), this, SLOT(on_setZeroPosition()));
        menu->addAction(set_zero_position);
 
+       if (view_.zero_offset().convert_to<double>() != 0) {
+               QAction *const reset_zero_position = new QAction(tr("Reset zero point"), this);
+               connect(reset_zero_position, SIGNAL(triggered()), this, SLOT(on_resetZeroPosition()));
+               menu->addAction(reset_zero_position);
+       }
+
        QAction *const toggle_hover_marker = new QAction(this);
        connect(toggle_hover_marker, SIGNAL(triggered()), this, SLOT(on_toggleHoverMarker()));
        menu->addAction(toggle_hover_marker);
@@ -166,18 +191,58 @@ vector< shared_ptr<ViewItem> > Ruler::items()
                time_items.begin(), time_items.end());
 }
 
+void Ruler::item_hover(const shared_ptr<ViewItem> &item, QPoint pos)
+{
+       (void)pos;
+
+       hover_item_ = dynamic_pointer_cast<TimeItem>(item);
+}
+
+shared_ptr<TimeItem> Ruler::get_reference_item() const
+{
+       // Note: time() returns 0 if item returns no valid time
+
+       if (mouse_modifiers_ & Qt::ShiftModifier)
+               return nullptr;
+
+       if (hover_item_ && (hover_item_->time() != 0))
+               return hover_item_;
+
+       shared_ptr<TimeItem> ref_item;
+       const vector< shared_ptr<TimeItem> > items(view_.time_items());
+
+       for (auto i = items.rbegin(); i != items.rend(); i++) {
+               if ((*i)->enabled() && (*i)->selected()) {
+                       if (!ref_item)
+                               ref_item = *i;
+                       else {
+                               // Return nothing if multiple items are selected
+                               ref_item.reset();
+                               break;
+                       }
+               }
+       }
+
+       if (ref_item && (ref_item->time() == 0))
+               ref_item.reset();
+
+       return ref_item;
+}
+
 shared_ptr<ViewItem> Ruler::get_mouse_over_item(const QPoint &pt)
 {
        const vector< shared_ptr<TimeItem> > items(view_.time_items());
+
        for (auto i = items.rbegin(); i != items.rend(); i++)
                if ((*i)->enabled() && (*i)->label_rect(rect()).contains(pt))
                        return *i;
+
        return nullptr;
 }
 
 void Ruler::mouseDoubleClickEvent(QMouseEvent *event)
 {
-       view_.add_flag(get_time_from_x_pos(event->x()));
+       hover_item_ = view_.add_flag(get_absolute_time_from_x_pos(event->x()));
 }
 
 void Ruler::paintEvent(QPaintEvent*)
@@ -330,12 +395,17 @@ void Ruler::invalidate_tick_position_cache()
 
 void Ruler::on_createMarker()
 {
-       view_.add_flag(get_time_from_x_pos(mouse_down_point_.x()));
+       hover_item_ = view_.add_flag(get_absolute_time_from_x_pos(mouse_down_point_.x()));
 }
 
 void Ruler::on_setZeroPosition()
 {
-       view_.set_zero_position(get_time_from_x_pos(mouse_down_point_.x()));
+       view_.set_zero_position(get_absolute_time_from_x_pos(mouse_down_point_.x()));
+}
+
+void Ruler::on_resetZeroPosition()
+{
+       view_.reset_zero_position();
 }
 
 void Ruler::on_toggleHoverMarker()
index b14148728c579b8c8f2885c8a76859fae976a3ca..9d708ce027bc9bfaca3ed6610c81fb338ea5dae6 100644 (file)
@@ -117,11 +117,18 @@ public:
                unsigned precision = 0,
                bool sign = true);
 
-       pv::util::Timestamp get_time_from_x_pos(uint32_t x) const;
+       pv::util::Timestamp get_absolute_time_from_x_pos(uint32_t x) const;
+       pv::util::Timestamp get_ruler_time_from_x_pos(uint32_t x) const;
+
+       pv::util::Timestamp get_ruler_time_from_absolute_time(const pv::util::Timestamp& abs_time) const;
+       pv::util::Timestamp get_absolute_time_from_ruler_time(const pv::util::Timestamp& ruler_time) const;
+
+       shared_ptr<TimeItem> get_reference_item() const;
 
 protected:
        virtual void contextMenuEvent(QContextMenuEvent *event) override;
        void resizeEvent(QResizeEvent*) override;
+       virtual void item_hover(const shared_ptr<ViewItem> &item, QPoint pos) override;
 
 private:
        /**
@@ -177,6 +184,7 @@ private Q_SLOTS:
 
        void on_createMarker();
        void on_setZeroPosition();
+       void on_resetZeroPosition();
        void on_toggleHoverMarker();
 
 private:
@@ -186,6 +194,8 @@ private:
         */
        boost::optional<TickPositions> tick_position_cache_;
 
+       shared_ptr<TimeItem> hover_item_;
+
        uint32_t context_menu_x_pos_;
 };
 
index 0c0cde05e18aa4d9d9f5cab9b748743b7fbffc7c..b770dee3acadd0d5a45cf38a323850aeed7bb665 100644 (file)
@@ -90,14 +90,34 @@ shared_ptr<data::SignalBase> Signal::base() const
 
 void Signal::save_settings(QSettings &settings) const
 {
-       (void)settings;
+       std::map<QString, QVariant> settings_map = save_settings();
+
+       for (auto& entry : settings_map)
+               settings.setValue(entry.first, entry.second);
+}
+
+std::map<QString, QVariant> Signal::save_settings() const
+{
+       return std::map<QString, QVariant>();
 }
 
 void Signal::restore_settings(QSettings &settings)
+{
+       std::map<QString, QVariant> settings_map;
+
+       QStringList keys = settings.allKeys();
+       for (int i = 0; i < keys.size(); i++)
+               settings_map[keys.at(i)] = settings.value(keys.at(i));
+
+       restore_settings(settings_map);
+}
+
+void Signal::restore_settings(std::map<QString, QVariant> settings)
 {
        (void)settings;
 }
 
+
 void Signal::paint_back(QPainter &p, ViewItemPaintParams &pp)
 {
        if (base_->enabled())
index 1b9f254330a79fd150f6f73165f5cb910a76b5be..c9f38dd96107657d7e16fd6fb05b71a16488dfa2 100644 (file)
@@ -23,6 +23,8 @@
 #include <memory>
 
 #include <QComboBox>
+#include <QString>
+#include <QVariant>
 #include <QWidgetAction>
 
 #include <cstdint>
@@ -87,8 +89,10 @@ public:
        shared_ptr<data::SignalBase> base() const;
 
        virtual void save_settings(QSettings &settings) const;
+       virtual std::map<QString, QVariant> save_settings() const;
 
        virtual void restore_settings(QSettings &settings);
+       virtual void restore_settings(std::map<QString, QVariant> settings);
 
        void paint_back(QPainter &p, ViewItemPaintParams &pp);
 
index 2269750600a984b2aba799cc9ca42f03015cb285..48d928919f54eb7d388ad6dcce2d6a4fc1d0023b 100644 (file)
@@ -203,7 +203,7 @@ void StandardBar::on_actionViewShowCursors_triggered()
        const bool show = action_view_show_cursors_->isChecked();
 
        if (show)
-               view_->centre_cursors();
+               view_->center_cursors();
 
        view_->show_cursors(show);
 }
index 47a7da3c503bbd616d5085a66b4ea5c8c103c74d..3de62258ae4fff0e3371f001036d15df81a9f628 100644 (file)
@@ -31,13 +31,28 @@ TimeItem::TimeItem(View &view) :
 
 void TimeItem::drag_by(const QPoint &delta)
 {
-       int64_t sample_num = view_.get_nearest_level_change(drag_point_ + delta);
-
-       if (sample_num > -1)
-               set_time(sample_num / view_.get_signal_under_mouse_cursor()->base()->get_samplerate());
-       else
+       if (snapping_disabled_) {
                set_time(view_.offset() + (drag_point_.x() + delta.x() - 0.5) *
                        view_.scale());
+       } else {
+               int64_t sample_num = view_.get_nearest_level_change(drag_point_ + delta);
+
+               if (sample_num > -1)
+                       set_time(sample_num / view_.get_signal_under_mouse_cursor()->base()->get_samplerate());
+               else
+                       set_time(view_.offset() + (drag_point_.x() + delta.x() - 0.5) * view_.scale());
+       }
+}
+
+const pv::util::Timestamp TimeItem::delta(const pv::util::Timestamp& other) const
+{
+       return other - time();
+}
+
+
+bool TimeItem::is_snapping_disabled() const
+{
+       return snapping_disabled_;
 }
 
 } // namespace trace
index ba858254f786aadf0477a61bfc495d1a53dd786e..06d850505ce8ebde350ecfbcbaeecbd5b3a79c3a 100644 (file)
@@ -43,20 +43,32 @@ protected:
         */
        TimeItem(View &view);
 
+       bool snapping_disabled_ = false;
+
 public:
        /**
         * Sets the time of the marker.
         */
        virtual void set_time(const pv::util::Timestamp& time) = 0;
 
+       /**
+        * Returns the time this time item is set to.
+        * @return 0 in case there is no valid time (e.g. for a cursor pair)
+        */
+       virtual const pv::util::Timestamp time() const = 0;
+
        virtual float get_x() const = 0;
 
+       virtual const pv::util::Timestamp delta(const pv::util::Timestamp& other) const;
+
        /**
         * Drags the item to a delta relative to the drag point.
         * @param delta the offset from the drag point.
         */
        void drag_by(const QPoint &delta);
 
+       bool is_snapping_disabled() const;
+
 protected:
        View &view_;
 };
index 266007a57053c220f59a87fe80eb630438a413c0..b428f6027e7bf4331d5c9d4d408a3f56fe554a92 100644 (file)
@@ -25,6 +25,7 @@
 #include "timemarker.hpp"
 
 #include "pv/widgets/timestampspinbox.hpp"
+#include "ruler.hpp"
 #include "view.hpp"
 
 #include <QApplication>
@@ -49,12 +50,11 @@ TimeMarker::TimeMarker(
        color_(color),
        time_(time),
        value_action_(nullptr),
-       value_widget_(nullptr),
-       updating_value_widget_(false)
+       value_widget_(nullptr)
 {
 }
 
-const pv::util::Timestamp& TimeMarker::time() const
+const pv::util::Timestamp TimeMarker::time() const
 {
        return time_;
 }
@@ -64,9 +64,8 @@ void TimeMarker::set_time(const pv::util::Timestamp& time)
        time_ = time;
 
        if (value_widget_) {
-               updating_value_widget_ = true;
-               value_widget_->setValue(time);
-               updating_value_widget_ = false;
+               QSignalBlocker blocker(value_widget_);
+               value_widget_->setValue(view_.ruler()->get_ruler_time_from_absolute_time(time));
        }
 
        view_.time_item_appearance_changed(true, true);
@@ -106,6 +105,11 @@ QRectF TimeMarker::hit_box_rect(const ViewItemPaintParams &pp) const
        return QRectF(x - h / 2.0f, pp.top(), h, pp.height());
 }
 
+void TimeMarker::set_text(const QString &text)
+{
+       (void)text;
+}
+
 void TimeMarker::paint_label(QPainter &p, const QRect &rect, bool hover)
 {
        if (!enabled())
@@ -179,7 +183,7 @@ pv::widgets::Popup* TimeMarker::create_popup(QWidget *parent)
        popup->setLayout(form);
 
        value_widget_ = new pv::widgets::TimestampSpinBox(parent);
-       value_widget_->setValue(time_);
+       value_widget_->setValue(view_.ruler()->get_ruler_time_from_absolute_time(time_));
 
        connect(value_widget_, SIGNAL(valueChanged(const pv::util::Timestamp&)),
                this, SLOT(on_value_changed(const pv::util::Timestamp&)));
@@ -191,8 +195,7 @@ pv::widgets::Popup* TimeMarker::create_popup(QWidget *parent)
 
 void TimeMarker::on_value_changed(const pv::util::Timestamp& value)
 {
-       if (!updating_value_widget_)
-               set_time(value);
+       set_time(view_.ruler()->get_absolute_time_from_ruler_time(value));
 }
 
 } // namespace trace
index 48afc16bb77e9c346ec75e80c06ab3564873ecc1..cd7c84d279747cddf13fb0a6fd58c11af5292b3f 100644 (file)
@@ -65,7 +65,7 @@ public:
        /**
         * Gets the time of the marker.
         */
-       const pv::util::Timestamp& time() const;
+       virtual const pv::util::Timestamp time() const override;
 
        /**
         * Sets the time of the marker.
@@ -99,6 +99,11 @@ public:
         */
        virtual QString get_text() const = 0;
 
+       /**
+        * Sets the text to show in the marker.
+        */
+       virtual void set_text(const QString &text);
+
        /**
         * Paints the marker's label to the ruler.
         * @param p The painter to draw with.
@@ -128,7 +133,6 @@ protected:
 
        QWidgetAction *value_action_;
        pv::widgets::TimestampSpinBox *value_widget_;
-       bool updating_value_widget_;
 };
 
 } // namespace trace
index 5c854aed17e2aa3db35f4b668ec6d5ad4b09662d..6d44da2c889d8babc73cd6e2054965a1aa980a6c 100644 (file)
@@ -292,12 +292,13 @@ void Trace::paint_back(QPainter &p, ViewItemPaintParams &pp)
 
 void Trace::paint_axis(QPainter &p, ViewItemPaintParams &pp, int y)
 {
+       bool was_antialiased = p.testRenderHint(QPainter::Antialiasing);
        p.setRenderHint(QPainter::Antialiasing, false);
 
        p.setPen(axis_pen_);
        p.drawLine(QPointF(pp.left(), y), QPointF(pp.right(), y));
 
-       p.setRenderHint(QPainter::Antialiasing, true);
+       p.setRenderHint(QPainter::Antialiasing, was_antialiased);
 }
 
 void Trace::add_color_option(QWidget *parent, QFormLayout *form)
@@ -328,10 +329,11 @@ void Trace::paint_hover_marker(QPainter &p)
 
        const pair<int, int> extents = v_extents();
 
+       bool was_antialiased = p.testRenderHint(QPainter::Antialiasing);
        p.setRenderHint(QPainter::Antialiasing, false);
        p.drawLine(x, get_visual_y() + extents.first,
                x, get_visual_y() + extents.second);
-       p.setRenderHint(QPainter::Antialiasing, true);
+       p.setRenderHint(QPainter::Antialiasing, was_antialiased);
 }
 
 void Trace::create_popup_form()
@@ -344,13 +346,28 @@ void Trace::create_popup_form()
        // handled, leaving the parent popup_ time to handle the change.
        if (popup_form_) {
                QWidget *suicidal = new QWidget();
-               suicidal->setLayout(popup_form_);
+               suicidal->setLayout(popup_->layout());
                suicidal->deleteLater();
        }
 
        // Repopulate the popup
-       popup_form_ = new QFormLayout(popup_);
-       popup_->setLayout(popup_form_);
+       widgets::QWidthAdjustingScrollArea* scrollarea = new widgets::QWidthAdjustingScrollArea();
+       QWidget* scrollarea_content = new QWidget(scrollarea);
+
+       scrollarea->setWidget(scrollarea_content);
+       scrollarea->setWidgetResizable(true);
+       scrollarea->setContentsMargins(0, 0, 0, 0);
+       scrollarea->setFrameShape(QFrame::NoFrame);
+       scrollarea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+       scrollarea_content->setContentsMargins(0, 0, 0, 0);
+
+       popup_->setLayout(new QVBoxLayout());
+       popup_->layout()->addWidget(scrollarea);
+       popup_->layout()->setContentsMargins(0, 0, 0, 0);
+
+       popup_form_ = new QFormLayout(scrollarea_content);
+       popup_form_->setSizeConstraint(QLayout::SetMinAndMaxSize);
+
        populate_popup_form(popup_, popup_form_);
 }
 
@@ -411,7 +428,7 @@ void Trace::on_create_marker_here() const
        const Ruler *ruler = view->ruler();
        QPoint p = ruler->mapFrom(view, QPoint(context_menu_x_pos_, 0));
 
-       view->add_flag(ruler->get_time_from_x_pos(p.x()));
+       view->add_flag(ruler->get_absolute_time_from_x_pos(p.x()));
 }
 
 } // namespace trace
index 3605aa1a3b9c3fd031ca32211f92498f55ed355b..51e2ba42914741178f245d6b0c4a947b9c3e0b4a 100644 (file)
@@ -24,7 +24,7 @@
 
 #include <QPropertyAnimation>
 
-#include "rowitem.hpp"
+#include "viewitem.hpp"
 
 using std::enable_shared_from_this;
 using std::pair;
@@ -35,7 +35,7 @@ namespace trace {
 
 class TraceTreeItemOwner;
 
-class TraceTreeItem : public RowItem,
+class TraceTreeItem : public ViewItem,
        public enable_shared_from_this<TraceTreeItem>
 {
        Q_OBJECT
@@ -94,7 +94,7 @@ public:
         * Sets the owner this trace in the view trace hierachy.
         * @param The new owner of the trace.
         */
-       void set_owner(TraceTreeItemOwner *owner);
+       virtual void set_owner(TraceTreeItemOwner *owner);
 
        /**
         * Gets the visual y-offset of the axis.
index ccf9b9123637ea498abfe800145eca070f03d211..95ea83732c7bb4743b0abfb6c1afa70f2384e50a 100644 (file)
@@ -114,7 +114,7 @@ void TraceTreeItemOwner::restack_items()
 {
        vector<shared_ptr<TraceTreeItem>> items(trace_tree_child_items());
 
-       // Sort by the centre line of the extents
+       // Sort by the center line of the extents
        stable_sort(items.begin(), items.end(),
                [](const shared_ptr<TraceTreeItem> &a, const shared_ptr<TraceTreeItem> &b) {
                        const auto aext = a->v_extents();
index 3311f3502ba2d92540939a3ad9c5102550d07dda..843ccd49b35475dc273e8d0ae3fbed9f5bd04d0c 100644 (file)
@@ -56,6 +56,11 @@ void TriggerMarker::set_time(const pv::util::Timestamp& time)
        view_.time_item_appearance_changed(true, true);
 }
 
+const pv::util::Timestamp TriggerMarker::time() const
+{
+       return time_;
+}
+
 float TriggerMarker::get_x() const
 {
        return ((time_ - view_.offset()) / view_.scale()).convert_to<float>();
index a97fefb0d3faf8c571e347f6b7ddfa240c31af39..222d3fb9f78c76450d75f9dde25b32f6dc1240c0 100644 (file)
@@ -67,6 +67,8 @@ public:
         */
        void set_time(const pv::util::Timestamp& time) override;
 
+       virtual const pv::util::Timestamp time() const override;
+
        float get_x() const override;
 
        /**
index 79277b4e17074708b0f1e3718ef6eeb4996d2c93..227fadfdb4197c334d084dd9a19edb37e4728181 100644 (file)
 #include <iterator>
 #include <unordered_set>
 
-#include <boost/archive/text_iarchive.hpp>
-#include <boost/archive/text_oarchive.hpp>
-#include <boost/serialization/serialization.hpp>
-
 #include <QApplication>
+#include <QDebug>
 #include <QEvent>
 #include <QFontMetrics>
 #include <QMenu>
@@ -84,7 +81,6 @@ using std::pair;
 using std::set;
 using std::set_difference;
 using std::shared_ptr;
-using std::stringstream;
 using std::unordered_map;
 using std::unordered_set;
 using std::vector;
@@ -98,8 +94,10 @@ const Timestamp View::MinScale("1e-12");
 
 const int View::MaxScrollValue = INT_MAX / 2;
 
-const int View::ScaleUnits[3] = {1, 2, 5};
+/* Area at the top and bottom of the view that can't be scrolled out of sight */
+const int View::ViewScrollMargin = 50;
 
+const int View::ScaleUnits[3] = {1, 2, 5};
 
 CustomScrollArea::CustomScrollArea(QWidget *parent) :
        QAbstractScrollArea(parent)
@@ -124,13 +122,14 @@ bool CustomScrollArea::viewportEvent(QEvent *event)
        }
 }
 
-View::View(Session &session, bool is_main_view, QWidget *parent) :
+View::View(Session &session, bool is_main_view, QMainWindow *parent) :
        ViewBase(session, is_main_view, parent),
 
        // Note: Place defaults in View::reset_view_state(), not here
        splitter_(new QSplitter()),
        header_was_shrunk_(false),  // The splitter remains unchanged after a reset, so this goes here
-       sticky_scrolling_(false)  // Default setting is set in MainWindow::setup_ui()
+       sticky_scrolling_(false),  // Default setting is set in MainWindow::setup_ui()
+       scroll_needs_defaults_(true)
 {
        QVBoxLayout *root_layout = new QVBoxLayout(this);
        root_layout->setContentsMargins(0, 0, 0, 0);
@@ -204,12 +203,53 @@ View::View(Session &session, bool is_main_view, QWidget *parent) :
        connect(&lazy_event_handler_, SIGNAL(timeout()),
                this, SLOT(process_sticky_events()));
        lazy_event_handler_.setSingleShot(true);
+       lazy_event_handler_.setInterval(1000 / ViewBase::MaxViewAutoUpdateRate);
+
+       // Set up local keyboard shortcuts
+       zoom_in_shortcut_ = new QShortcut(QKeySequence(Qt::Key_Plus), this,
+               SLOT(on_zoom_in_shortcut_triggered()), nullptr, Qt::WidgetWithChildrenShortcut);
+       zoom_in_shortcut_->setAutoRepeat(false);
+
+       zoom_out_shortcut_ = new QShortcut(QKeySequence(Qt::Key_Minus), this,
+               SLOT(on_zoom_out_shortcut_triggered()), nullptr, Qt::WidgetWithChildrenShortcut);
+       zoom_out_shortcut_->setAutoRepeat(false);
+
+       zoom_in_shortcut_2_ = new QShortcut(QKeySequence(Qt::Key_Up), this,
+               SLOT(on_zoom_in_shortcut_triggered()), nullptr, Qt::WidgetWithChildrenShortcut);
+       zoom_out_shortcut_2_ = new QShortcut(QKeySequence(Qt::Key_Down), this,
+               SLOT(on_zoom_out_shortcut_triggered()), nullptr, Qt::WidgetWithChildrenShortcut);
+
+       home_shortcut_ = new QShortcut(QKeySequence(Qt::Key_Home), this,
+               SLOT(on_scroll_to_start_shortcut_triggered()), nullptr, Qt::WidgetWithChildrenShortcut);
+       home_shortcut_->setAutoRepeat(false);
+
+       end_shortcut_ = new QShortcut(QKeySequence(Qt::Key_End), this,
+               SLOT(on_scroll_to_end_shortcut_triggered()), nullptr, Qt::WidgetWithChildrenShortcut);
+       end_shortcut_->setAutoRepeat(false);
+
+       grab_ruler_left_shortcut_ = new QShortcut(QKeySequence(Qt::Key_1), this,
+               nullptr, nullptr, Qt::WidgetWithChildrenShortcut);
+       connect(grab_ruler_left_shortcut_, &QShortcut::activated,
+               this, [=]{on_grab_ruler(1);});
+       grab_ruler_left_shortcut_->setAutoRepeat(false);
+
+       grab_ruler_right_shortcut_ = new QShortcut(QKeySequence(Qt::Key_2), this,
+               nullptr, nullptr, Qt::WidgetWithChildrenShortcut);
+       connect(grab_ruler_right_shortcut_, &QShortcut::activated,
+               this, [=]{on_grab_ruler(2);});
+       grab_ruler_right_shortcut_->setAutoRepeat(false);
+
+       cancel_grab_shortcut_ = new QShortcut(QKeySequence(Qt::Key_Escape), this,
+               nullptr, nullptr, Qt::WidgetWithChildrenShortcut);
+       connect(cancel_grab_shortcut_, &QShortcut::activated,
+               this, [=]{grabbed_widget_ = nullptr;});
+       cancel_grab_shortcut_->setAutoRepeat(false);
 
        // Trigger the initial event manually. The default device has signals
        // which were created before this object came into being
        signals_changed();
 
-       // make sure the transparent widgets are on the top
+       // Make sure the transparent widgets are on the top
        ruler_->raise();
        header_->raise();
 
@@ -221,6 +261,11 @@ View::~View()
        GlobalSettings::remove_change_handler(this);
 }
 
+ViewType View::get_type() const
+{
+       return ViewTypeTrace;
+}
+
 void View::reset_view_state()
 {
        ViewBase::reset_view_state();
@@ -230,6 +275,8 @@ void View::reset_view_state()
        scale_ = 1e-3;
        offset_ = 0;
        ruler_offset_ = 0;
+       zero_offset_ = 0;
+       custom_zero_offset_set_ = false;
        updating_scroll_ = false;
        settings_restored_ = false;
        always_zoom_to_fit_ = false;
@@ -242,6 +289,7 @@ void View::reset_view_state()
        next_flag_text_ = 'A';
        trigger_markers_.clear();
        hover_widget_ = nullptr;
+       grabbed_widget_ = nullptr;
        hover_point_ = QPoint(-1, -1);
        scroll_needs_defaults_ = true;
        saved_v_offset_ = 0;
@@ -270,21 +318,34 @@ const Session& View::session() const
        return session_;
 }
 
-unordered_set< shared_ptr<Signal> > View::signals() const
+vector< shared_ptr<Signal> > View::signals() const
 {
        return signals_;
 }
 
+shared_ptr<Signal> View::get_signal_by_signalbase(shared_ptr<data::SignalBase> base) const
+{
+       shared_ptr<Signal> ret_val;
+
+       for (const shared_ptr<Signal>& s : signals_)
+               if (s->base() == base) {
+                       ret_val = s;
+                       break;
+               }
+
+       return ret_val;
+}
+
 void View::clear_signals()
 {
-       ViewBase::clear_signalbases();
+       ViewBase::clear_signals();
        signals_.clear();
 }
 
 void View::add_signal(const shared_ptr<Signal> signal)
 {
        ViewBase::add_signalbase(signal->base());
-       signals_.insert(signal);
+       signals_.push_back(signal);
 
        signal->set_segment_display_mode(segment_display_mode_);
        signal->set_current_segment(current_segment_);
@@ -296,11 +357,14 @@ void View::add_signal(const shared_ptr<Signal> signal)
 #ifdef ENABLE_DECODE
 void View::clear_decode_signals()
 {
+       ViewBase::clear_decode_signals();
        decode_traces_.clear();
 }
 
 void View::add_decode_signal(shared_ptr<data::DecodeSignal> signal)
 {
+       ViewBase::add_decode_signal(signal);
+
        shared_ptr<DecodeTrace> d(
                new DecodeTrace(session_, signal, decode_traces_.size()));
        decode_traces_.push_back(d);
@@ -320,6 +384,8 @@ void View::remove_decode_signal(shared_ptr<data::DecodeSignal> signal)
                        signals_changed();
                        return;
                }
+
+       ViewBase::remove_decode_signal(signal);
 }
 #endif
 
@@ -348,6 +414,11 @@ const Viewport* View::viewport() const
        return viewport_;
 }
 
+QAbstractScrollArea* View::scrollarea() const
+{
+       return scrollarea_;
+}
+
 const Ruler* View::ruler() const
 {
        return ruler_;
@@ -362,18 +433,12 @@ void View::save_settings(QSettings &settings) const
        settings.setValue("splitter_state", splitter_->saveState());
        settings.setValue("segment_display_mode", segment_display_mode_);
 
-       {
-               stringstream ss;
-               boost::archive::text_oarchive oa(ss);
-               oa << boost::serialization::make_nvp("ruler_shift", ruler_shift_);
-               settings.setValue("ruler_shift", QString::fromStdString(ss.str()));
-       }
-       {
-               stringstream ss;
-               boost::archive::text_oarchive oa(ss);
-               oa << boost::serialization::make_nvp("offset", offset_);
-               settings.setValue("offset", QString::fromStdString(ss.str()));
-       }
+       GlobalSettings::store_timestamp(settings, "offset", offset_);
+
+       if (custom_zero_offset_set_)
+               GlobalSettings::store_timestamp(settings, "zero_offset", -zero_offset_);
+       else
+               settings.remove("zero_offset");
 
        for (const shared_ptr<Signal>& signal : signals_) {
                settings.beginGroup(signal->base()->internal_name());
@@ -390,35 +455,14 @@ void View::restore_settings(QSettings &settings)
        if (settings.contains("scale"))
                set_scale(settings.value("scale").toDouble());
 
-       if (settings.contains("ruler_shift")) {
-               util::Timestamp shift;
-               stringstream ss;
-               ss << settings.value("ruler_shift").toString().toStdString();
-
-               try {
-                       boost::archive::text_iarchive ia(ss);
-                       ia >> boost::serialization::make_nvp("ruler_shift", shift);
-                       ruler_shift_ = shift;
-               } catch (boost::archive::archive_exception&) {
-                       qDebug() << "Could not restore the view ruler shift";
-               }
-       }
-
        if (settings.contains("offset")) {
-               util::Timestamp offset;
-               stringstream ss;
-               ss << settings.value("offset").toString().toStdString();
-
-               try {
-                       boost::archive::text_iarchive ia(ss);
-                       ia >> boost::serialization::make_nvp("offset", offset);
-                       // This also updates ruler_offset_
-                       set_offset(offset);
-               } catch (boost::archive::archive_exception&) {
-                       qDebug() << "Could not restore the view offset";
-               }
+               // This also updates ruler_offset_
+               set_offset(GlobalSettings::restore_timestamp(settings, "offset"));
        }
 
+       if (settings.contains("zero_offset"))
+               set_zero_position(GlobalSettings::restore_timestamp(settings, "zero_offset"));
+
        if (settings.contains("splitter_state"))
                splitter_->restoreState(settings.value("splitter_state").toByteArray());
 
@@ -480,7 +524,7 @@ void View::set_offset(const pv::util::Timestamp& offset, bool force_update)
 {
        if ((offset_ != offset) || force_update) {
                offset_ = offset;
-               ruler_offset_ = offset_ + ruler_shift_;
+               ruler_offset_ = offset_ + zero_offset_;
                offset_changed();
        }
 }
@@ -497,10 +541,8 @@ const Timestamp& View::ruler_offset() const
 
 void View::set_zero_position(const pv::util::Timestamp& position)
 {
-       // ruler shift is a negative offset and the new zero position is relative
-       // to the current offset. Hence, we adjust the ruler shift only by the
-       // difference.
-       ruler_shift_ = -(position + (-ruler_shift_));
+       zero_offset_ = -position;
+       custom_zero_offset_set_ = true;
 
        // Force an immediate update of the offsets
        set_offset(offset_, true);
@@ -509,13 +551,31 @@ void View::set_zero_position(const pv::util::Timestamp& position)
 
 void View::reset_zero_position()
 {
-       ruler_shift_ = 0;
+       zero_offset_ = 0;
+
+       // When enabled, the first trigger for this segment is used as the zero position
+       GlobalSettings settings;
+       bool trigger_is_zero_time = settings.value(GlobalSettings::Key_View_TriggerIsZeroTime).toBool();
+
+       if (trigger_is_zero_time) {
+               vector<util::Timestamp> triggers = session_.get_triggers(current_segment_);
+
+               if (triggers.size() > 0)
+                       zero_offset_ = triggers.front();
+       }
+
+       custom_zero_offset_set_ = false;
 
        // Force an immediate update of the offsets
        set_offset(offset_, true);
        ruler_->update();
 }
 
+pv::util::Timestamp View::zero_offset() const
+{
+       return zero_offset_;
+}
+
 int View::owner_visual_v_offset() const
 {
        return -scrollarea_->verticalScrollBar()->sliderPosition();
@@ -528,6 +588,18 @@ void View::set_v_offset(int offset)
        viewport_->update();
 }
 
+void View::set_h_offset(int offset)
+{
+       scrollarea_->horizontalScrollBar()->setSliderPosition(offset);
+       header_->update();
+       viewport_->update();
+}
+
+int View::get_h_scrollbar_maximum() const
+{
+       return scrollarea_->horizontalScrollBar()->maximum();
+}
+
 unsigned int View::depth() const
 {
        return 0;
@@ -612,12 +684,8 @@ void View::set_current_segment(uint32_t segment_id)
        for (util::Timestamp timestamp : triggers)
                trigger_markers_.push_back(make_shared<TriggerMarker>(*this, timestamp));
 
-       // When enabled, the first trigger for this segment is used as the zero position
-       GlobalSettings settings;
-       bool trigger_is_zero_time = settings.value(GlobalSettings::Key_View_TriggerIsZeroTime).toBool();
-
-       if (trigger_is_zero_time && (triggers.size() > 0))
-               set_zero_position(triggers.front());
+       if (!custom_zero_offset_set_)
+               reset_zero_position();
 
        viewport_->update();
 
@@ -743,13 +811,13 @@ void View::set_scale_offset(double scale, const Timestamp& offset)
        viewport_->update();
 }
 
-set< shared_ptr<SignalData> > View::get_visible_data() const
+vector< shared_ptr<SignalData> > View::get_visible_data() const
 {
        // Make a set of all the visible data objects
-       set< shared_ptr<SignalData> > visible_data;
+       vector< shared_ptr<SignalData> > visible_data;
        for (const shared_ptr<Signal>& sig : signals_)
                if (sig->enabled())
-                       visible_data.insert(sig->data());
+                       visible_data.push_back(sig->data());
 
        return visible_data;
 }
@@ -757,8 +825,14 @@ set< shared_ptr<SignalData> > View::get_visible_data() const
 pair<Timestamp, Timestamp> View::get_time_extents() const
 {
        boost::optional<Timestamp> left_time, right_time;
-       const set< shared_ptr<SignalData> > visible_data = get_visible_data();
-       for (const shared_ptr<SignalData>& d : visible_data) {
+
+       vector< shared_ptr<SignalData> > data;
+       if (signals_.size() == 0)
+               return make_pair(0, 0);
+
+       data.push_back(signals_.front()->data());
+
+       for (const shared_ptr<SignalData>& d : data) {
                const vector< shared_ptr<Segment> > segments = d->segments();
                for (const shared_ptr<Segment>& s : segments) {
                        double samplerate = s->samplerate();
@@ -781,26 +855,6 @@ pair<Timestamp, Timestamp> View::get_time_extents() const
        return make_pair(*left_time, *right_time);
 }
 
-void View::enable_show_sampling_points(bool state)
-{
-       (void)state;
-
-       viewport_->update();
-}
-
-void View::enable_show_analog_minor_grid(bool state)
-{
-       (void)state;
-
-       viewport_->update();
-}
-
-void View::enable_colored_bg(bool state)
-{
-       colored_bg_ = state;
-       viewport_->update();
-}
-
 bool View::colored_bg() const
 {
        return colored_bg_;
@@ -813,22 +867,36 @@ bool View::cursors_shown() const
 
 void View::show_cursors(bool show)
 {
-       show_cursors_ = show;
-       cursor_state_changed(show);
+       if (show_cursors_ != show) {
+               show_cursors_ = show;
+
+               cursor_state_changed(show);
+               ruler_->update();
+               viewport_->update();
+       }
+}
+
+void View::set_cursors(pv::util::Timestamp& first, pv::util::Timestamp& second)
+{
+       assert(cursors_);
+
+       cursors_->first()->set_time(first);
+       cursors_->second()->set_time(second);
+
        ruler_->update();
        viewport_->update();
 }
 
-void View::centre_cursors()
+void View::center_cursors()
 {
-       if (cursors_) {
-               const double time_width = scale_ * viewport_->width();
-               cursors_->first()->set_time(offset_ + time_width * 0.4);
-               cursors_->second()->set_time(offset_ + time_width * 0.6);
+       assert(cursors_);
 
-               ruler_->update();
-               viewport_->update();
-       }
+       const double time_width = scale_ * viewport_->width();
+       cursors_->first()->set_time(offset_ + time_width * 0.4);
+       cursors_->second()->set_time(offset_ + time_width * 0.6);
+
+       ruler_->update();
+       viewport_->update();
 }
 
 shared_ptr<CursorPair> View::cursors() const
@@ -836,15 +904,17 @@ shared_ptr<CursorPair> View::cursors() const
        return cursors_;
 }
 
-void View::add_flag(const Timestamp& time)
+shared_ptr<Flag> View::add_flag(const Timestamp& time)
 {
-       flags_.push_back(make_shared<Flag>(*this, time,
-               QString("%1").arg(next_flag_text_)));
+       shared_ptr<Flag> flag =
+               make_shared<Flag>(*this, time, QString("%1").arg(next_flag_text_));
+       flags_.push_back(flag);
 
        next_flag_text_ = (next_flag_text_ >= 'Z') ? 'A' :
                (next_flag_text_ + 1);
 
        time_item_appearance_changed(true, true);
+       return flag;
 }
 
 void View::remove_flag(shared_ptr<Flag> flag)
@@ -1004,13 +1074,22 @@ int View::header_width() const
 
 void View::on_setting_changed(const QString &key, const QVariant &value)
 {
+       GlobalSettings settings;
+
+       if (key == GlobalSettings::Key_View_ColoredBG) {
+               colored_bg_ = settings.value(GlobalSettings::Key_View_ColoredBG).toBool();
+               viewport_->update();
+       }
+
+       if ((key == GlobalSettings::Key_View_ShowSamplingPoints) ||
+          (key == GlobalSettings::Key_View_ShowAnalogMinorGrid))
+               viewport_->update();
+
        if (key == GlobalSettings::Key_View_TriggerIsZeroTime)
                on_settingViewTriggerIsZeroTime_changed(value);
 
-       if (key == GlobalSettings::Key_View_SnapDistance) {
-               GlobalSettings settings;
+       if (key == GlobalSettings::Key_View_SnapDistance)
                snap_distance_ = settings.value(GlobalSettings::Key_View_SnapDistance).toInt();
-       }
 }
 
 void View::trigger_event(int segment_id, util::Timestamp location)
@@ -1019,15 +1098,8 @@ void View::trigger_event(int segment_id, util::Timestamp location)
        if ((uint32_t)segment_id != current_segment_)
                return;
 
-       // Set zero location if the Key_View_TriggerIsZeroTime setting is set and
-       // if this is the first trigger for this segment.
-       GlobalSettings settings;
-       bool trigger_is_zero_time = settings.value(GlobalSettings::Key_View_TriggerIsZeroTime).toBool();
-
-       size_t trigger_count = session_.get_triggers(current_segment_).size();
-
-       if (trigger_is_zero_time && trigger_count == 1)
-               set_zero_position(location);
+       if (!custom_zero_offset_set_)
+               reset_zero_position();
 
        trigger_markers_.push_back(make_shared<TriggerMarker>(*this, location));
 }
@@ -1182,9 +1254,13 @@ void View::update_scroll()
        const pair<int, int> extents = v_extents();
 
        // Don't change the scrollbar range if there are no traces
-       if (extents.first != extents.second)
-               vscrollbar->setRange(extents.first - areaSize.height(),
-                       extents.second);
+       if (extents.first != extents.second) {
+               int top_margin = ViewScrollMargin;
+               int btm_margin = ViewScrollMargin;
+
+               vscrollbar->setRange(extents.first - areaSize.height() + top_margin,
+                       extents.second - btm_margin);
+       }
 
        if (scroll_needs_defaults_) {
                set_scroll_default();
@@ -1337,6 +1413,7 @@ void View::determine_time_unit()
 bool View::eventFilter(QObject *object, QEvent *event)
 {
        const QEvent::Type type = event->type();
+
        if (type == QEvent::MouseMove) {
 
                if (object)
@@ -1354,6 +1431,28 @@ bool View::eventFilter(QObject *object, QEvent *event)
 
                update_hover_point();
 
+               if (grabbed_widget_) {
+                       int64_t nearest = get_nearest_level_change(hover_point_);
+                       pv::util::Timestamp mouse_time = offset_ + hover_point_.x() * scale_;
+
+                       if (nearest == -1) {
+                               grabbed_widget_->set_time(mouse_time);
+                       } else {
+                               grabbed_widget_->set_time(nearest / get_signal_under_mouse_cursor()->base()->get_samplerate());
+                       }
+               }
+
+       } else if (type == QEvent::MouseButtonPress) {
+               grabbed_widget_ = nullptr;
+
+               const QMouseEvent *const mouse_event = (QMouseEvent*)event;
+               if ((object == viewport_) && (mouse_event->button() & Qt::LeftButton)) {
+                       // Send event to all trace tree items
+                       const vector<shared_ptr<TraceTreeItem>> trace_tree_items(
+                               list_by_type<TraceTreeItem>());
+                       for (const shared_ptr<TraceTreeItem>& r : trace_tree_items)
+                               r->mouse_left_press_event(mouse_event);
+               }
        } else if (type == QEvent::Leave) {
                hover_point_ = QPoint(-1, -1);
                update_hover_point();
@@ -1461,8 +1560,8 @@ void View::extents_changed(bool horz, bool vert)
                (horz ? TraceTreeItemHExtentsChanged : 0) |
                (vert ? TraceTreeItemVExtentsChanged : 0);
 
-       lazy_event_handler_.stop();
-       lazy_event_handler_.start();
+       if (!lazy_event_handler_.isActive())
+               lazy_event_handler_.start();
 }
 
 void View::on_signal_name_changed()
@@ -1480,6 +1579,26 @@ void View::on_splitter_moved()
                resize_header_to_fit();
 }
 
+void View::on_zoom_in_shortcut_triggered()
+{
+       zoom(1);
+}
+
+void View::on_zoom_out_shortcut_triggered()
+{
+       zoom(-1);
+}
+
+void View::on_scroll_to_start_shortcut_triggered()
+{
+       set_h_offset(0);
+}
+
+void View::on_scroll_to_end_shortcut_triggered()
+{
+       set_h_offset(get_h_scrollbar_maximum());
+}
+
 void View::h_scroll_value_changed(int value)
 {
        if (updating_scroll_)
@@ -1512,6 +1631,26 @@ void View::v_scroll_value_changed()
        viewport_->update();
 }
 
+void View::on_grab_ruler(int ruler_id)
+{
+       if (!cursors_shown()) {
+               center_cursors();
+               show_cursors();
+       }
+
+       // Release the grabbed widget if its trigger hotkey was pressed twice
+       if (ruler_id == 1)
+               grabbed_widget_ = (grabbed_widget_ == cursors_->first().get()) ?
+                       nullptr : cursors_->first().get();
+       else
+               grabbed_widget_ = (grabbed_widget_ == cursors_->second().get()) ?
+                       nullptr : cursors_->second().get();
+
+       if (grabbed_widget_)
+               grabbed_widget_->set_time(ruler_->get_absolute_time_from_x_pos(
+                       mapFromGlobal(QCursor::pos()).x() - header_width()));
+}
+
 void View::signals_changed()
 {
        using sigrok::Channel;
@@ -1699,6 +1838,8 @@ void View::capture_state_updated(int state)
                set_time_unit(util::TimeUnit::Samples);
 
                trigger_markers_.clear();
+               if (!custom_zero_offset_set_)
+                       set_zero_position(0);
 
                scale_at_acq_start_ = scale_;
                offset_at_acq_start_ = offset_;
@@ -1781,12 +1922,9 @@ void View::on_segment_changed(int segment)
 
 void View::on_settingViewTriggerIsZeroTime_changed(const QVariant new_value)
 {
-       if (new_value.toBool()) {
-               // The first trigger for this segment is used as the zero position
-               vector<util::Timestamp> triggers = session_.get_triggers(current_segment_);
-               if (triggers.size() > 0)
-                       set_zero_position(triggers.front());
-       } else
+       (void)new_value;
+
+       if (!custom_zero_offset_set_)
                reset_zero_position();
 }
 
index a6655d2f492ab941617fc95f7c694463b1aa6645..d57acd914bdaa8872a95f930e7e8b9b374fc3d4d 100644 (file)
@@ -28,6 +28,7 @@
 #include <vector>
 
 #include <QAbstractScrollArea>
+#include <QShortcut>
 #include <QSizeF>
 #include <QSplitter>
 
@@ -43,7 +44,6 @@
 
 using std::list;
 using std::unordered_map;
-using std::unordered_set;
 using std::set;
 using std::shared_ptr;
 using std::vector;
@@ -95,27 +95,32 @@ private:
        static const pv::util::Timestamp MinScale;
 
        static const int MaxScrollValue;
+       static const int ViewScrollMargin;
 
        static const int ScaleUnits[3];
 
 public:
-       explicit View(Session &session, bool is_main_view=false, QWidget *parent = nullptr);
+       explicit View(Session &session, bool is_main_view=false, QMainWindow *parent = nullptr);
 
        ~View();
 
+       virtual ViewType get_type() const;
+
        /**
         * Resets the view to its default state after construction. It does however
         * not reset the signal bases or any other connections with the session.
         */
        virtual void reset_view_state();
 
-       Session& session();
-       const Session& session() const;
+       Session& session();  // This method is needed for TraceTreeItemOwner, not ViewBase
+       const Session& session() const;  // This method is needed for TraceTreeItemOwner, not ViewBase
 
        /**
         * Returns the signals contained in this view.
         */
-       unordered_set< shared_ptr<Signal> > signals() const;
+       vector< shared_ptr<Signal> > signals() const;
+
+       shared_ptr<Signal> get_signal_by_signalbase(shared_ptr<data::SignalBase> base) const;
 
        virtual void clear_signals();
 
@@ -142,9 +147,10 @@ public:
        virtual const View* view() const;
 
        Viewport* viewport();
-
        const Viewport* viewport() const;
 
+       QAbstractScrollArea* scrollarea() const;
+
        const Ruler* ruler() const;
 
        virtual void save_settings(QSettings &settings) const;
@@ -177,6 +183,8 @@ public:
 
        void reset_zero_position();
 
+       pv::util::Timestamp zero_offset() const;
+
        /**
         * Returns the vertical scroll offset.
         */
@@ -187,6 +195,16 @@ public:
         */
        void set_v_offset(int offset);
 
+       /**
+        * Sets the visual h-offset.
+        */
+       void set_h_offset(int offset);
+
+       /**
+        * Gets the length of the horizontal scrollbar.
+        */
+       int get_h_scrollbar_maximum() const;
+
        /**
         * Returns the SI prefix to apply to the graticule time markings.
         */
@@ -243,33 +261,17 @@ public:
         */
        void set_scale_offset(double scale, const pv::util::Timestamp& offset);
 
-       set< shared_ptr<pv::data::SignalData> > get_visible_data() const;
+       vector< shared_ptr<pv::data::SignalData> > get_visible_data() const;
 
        pair<pv::util::Timestamp, pv::util::Timestamp> get_time_extents() const;
 
-       /**
-        * Enables or disables colored trace backgrounds. If they're not
-        * colored then they will use alternating colors.
-        */
-       void enable_colored_bg(bool state);
-
        /**
         * Returns true if the trace background should be drawn with a colored background.
         */
        bool colored_bg() const;
 
        /**
-        * Enable or disable showing sampling points.
-        */
-       void enable_show_sampling_points(bool state);
-
-       /**
-        * Enable or disable showing the analog minor grid.
-        */
-       void enable_show_analog_minor_grid(bool state);
-
-       /**
-        * Returns true if cursors are displayed. false otherwise.
+        * Returns true if cursors are displayed, false otherwise.
         */
        bool cursors_shown() const;
 
@@ -278,10 +280,17 @@ public:
         */
        void show_cursors(bool show = true);
 
+       /**
+        * Sets the cursors to the given offsets.
+        * You still have to call show_cursors() separately.
+        */
+       void set_cursors(pv::util::Timestamp& first, pv::util::Timestamp& second);
+
        /**
         * Moves the cursors to a convenient position in the view.
+        * You still have to call show_cursors() separately.
         */
-       void centre_cursors();
+       void center_cursors();
 
        /**
         * Returns a reference to the pair of cursors.
@@ -291,7 +300,7 @@ public:
        /**
         * Adds a new flag at a specified time.
         */
-       void add_flag(const pv::util::Timestamp& time);
+       shared_ptr<Flag> add_flag(const pv::util::Timestamp& time);
 
        /**
         * Removes a flag from the list.
@@ -421,13 +430,19 @@ public:
        void extents_changed(bool horz, bool vert);
 
 private Q_SLOTS:
-
        void on_signal_name_changed();
        void on_splitter_moved();
 
+       void on_zoom_in_shortcut_triggered();
+       void on_zoom_out_shortcut_triggered();
+       void on_scroll_to_start_shortcut_triggered();
+       void on_scroll_to_end_shortcut_triggered();
+
        void h_scroll_value_changed(int value);
        void v_scroll_value_changed();
 
+       void on_grab_ruler(int ruler_id);
+
        void signals_changed();
        void capture_state_updated(int state);
 
@@ -489,7 +504,13 @@ private:
        Header *header_;
        QSplitter *splitter_;
 
-       unordered_set< shared_ptr<Signal> > signals_;
+       QShortcut *zoom_in_shortcut_, *zoom_in_shortcut_2_;
+       QShortcut *zoom_out_shortcut_, *zoom_out_shortcut_2_;
+       QShortcut *home_shortcut_, *end_shortcut_;
+       QShortcut *grab_ruler_left_shortcut_, *grab_ruler_right_shortcut_;
+       QShortcut *cancel_grab_shortcut_;
+
+       vector< shared_ptr<Signal> > signals_;
 
 #ifdef ENABLE_DECODE
        vector< shared_ptr<DecodeTrace> > decode_traces_;
@@ -507,6 +528,10 @@ private:
        pv::util::Timestamp offset_;
        /// The ruler version of the time offset in seconds.
        pv::util::Timestamp ruler_offset_;
+       /// The offset of the zero point in seconds.
+       pv::util::Timestamp zero_offset_;
+       /// Shows whether the user set a custom zero offset that we should keep
+       bool custom_zero_offset_set_;
 
        bool updating_scroll_;
        bool settings_restored_;
@@ -531,6 +556,7 @@ private:
        vector< shared_ptr<TriggerMarker> > trigger_markers_;
 
        QWidget* hover_widget_;
+       TimeMarker* grabbed_widget_;
        QPoint hover_point_;
        shared_ptr<Signal> signal_under_mouse_cursor_;
        uint16_t snap_distance_;
index 2dd8ade8c1fddb03c718c79179c4161c278dc461..f84080ec79fe92c9207dd6d713135c4c15c3c9b9 100644 (file)
@@ -148,6 +148,16 @@ QColor ViewItem::select_text_color(QColor background)
        return (background.lightness() > 110) ? Qt::black : Qt::white;
 }
 
+void ViewItem::hover_point_changed(const QPoint &hp)
+{
+       (void)hp;
+}
+
+void ViewItem::mouse_left_press_event(const QMouseEvent* event)
+{
+       (void)event;
+}
+
 } // namespace trace
 } // namespace views
 } // namespace pv
index 5ce3bb608ec68156ba06c53b12409619e9772ec9..423e75d2f313e38ec7cd156831c1119edaea47f8 100644 (file)
@@ -22,6 +22,7 @@
 
 #include <list>
 
+#include <QMouseEvent>
 #include <QPen>
 #include <QPoint>
 
@@ -167,6 +168,14 @@ public:
 
        virtual void delete_pressed();
 
+       virtual void hover_point_changed(const QPoint &hp);
+
+       /**
+        * Handles left mouse button press events.
+        * @param event the mouse event that triggered this handler.
+        */
+       virtual void mouse_left_press_event(const QMouseEvent* event);
+
 protected:
        static QPen highlight_pen();
 
index 83abb7b6cbd5273f699a204d2720e4404f6c00e2..0c73ec494adfbc72257252c59ead85d609a6ad24 100644 (file)
@@ -30,6 +30,8 @@
 #include <pv/session.hpp>
 
 #include <QMouseEvent>
+#include <QScreen>
+#include <QWindow>
 
 #include <QDebug>
 
@@ -67,8 +69,8 @@ shared_ptr<ViewItem> Viewport::get_mouse_over_item(const QPoint &pt)
 void Viewport::item_hover(const shared_ptr<ViewItem> &item, QPoint pos)
 {
        if (item && item->is_draggable(pos))
-               setCursor(dynamic_pointer_cast<RowItem>(item) ?
-                       Qt::SizeVerCursor : Qt::SizeHorCursor);
+               setCursor(dynamic_pointer_cast<ViewItem>(item) ?
+                       Qt::SizeHorCursor : Qt::SizeVerCursor);
        else
                unsetCursor();
 }
@@ -114,6 +116,9 @@ bool Viewport::touch_event(QTouchEvent *event)
                pinch_zoom_active_ = false;
                return false;
        }
+       if (event->device()->type() == QTouchDevice::TouchPad) {
+               return false;
+       }
 
        const QTouchEvent::TouchPoint &touchPoint0 = touchPoints.first();
        const QTouchEvent::TouchPoint &touchPoint1 = touchPoints.last();
@@ -159,12 +164,12 @@ void Viewport::paintEvent(QPaintEvent*)
                &ViewItem::paint_back, &ViewItem::paint_mid,
                &ViewItem::paint_fore, nullptr};
 
-       vector< shared_ptr<RowItem> > row_items(view_.list_by_type<RowItem>());
+       vector< shared_ptr<ViewItem> > row_items(view_.list_by_type<ViewItem>());
        assert(none_of(row_items.begin(), row_items.end(),
-               [](const shared_ptr<RowItem> &r) { return !r; }));
+               [](const shared_ptr<ViewItem> &r) { return !r; }));
 
        stable_sort(row_items.begin(), row_items.end(),
-               [](const shared_ptr<RowItem> &a, const shared_ptr<RowItem> &b) {
+               [](const shared_ptr<ViewItem> &a, const shared_ptr<ViewItem> &b) {
                        return a->drag_point(QRect()).y() < b->drag_point(QRect()).y(); });
 
        const vector< shared_ptr<TimeItem> > time_items(view_.time_items());
@@ -172,7 +177,11 @@ void Viewport::paintEvent(QPaintEvent*)
                [](const shared_ptr<TimeItem> &t) { return !t; }));
 
        QPainter p(this);
-       p.setRenderHint(QPainter::Antialiasing);
+
+       // Disable antialiasing for high-DPI displays
+       bool use_antialiasing =
+               window()->windowHandle()->screen()->devicePixelRatio() < 2.0;
+       p.setRenderHint(QPainter::Antialiasing, use_antialiasing);
 
        for (LayerPaintFunc *paint_func = layer_paint_funcs;
                        *paint_func; paint_func++) {
@@ -181,7 +190,7 @@ void Viewport::paintEvent(QPaintEvent*)
                        (t.get()->*(*paint_func))(p, time_pp);
 
                ViewItemPaintParams row_pp(rect(), view_.scale(), view_.offset());
-               for (const shared_ptr<RowItem>& r : row_items)
+               for (const shared_ptr<ViewItem>& r : row_items)
                        (r.get()->*(*paint_func))(p, row_pp);
        }
 
index 73c361f47d802dd469b8ad59f9a2f4e47f285060..5a85c15d5c52d25e29aeff47e1af3e70dc0ec60d 100644 (file)
@@ -105,9 +105,9 @@ void ViewWidget::drag_items(const QPoint &delta)
        bool item_dragged = false;
 
        // Drag the row items
-       const vector< shared_ptr<RowItem> > row_items(
-               view_.list_by_type<RowItem>());
-       for (const shared_ptr<RowItem>& r : row_items)
+       const vector< shared_ptr<ViewItem> > row_items(
+               view_.list_by_type<ViewItem>());
+       for (const shared_ptr<ViewItem>& r : row_items)
                if (r->dragging()) {
                        r->drag_by(delta);
 
@@ -253,7 +253,11 @@ void ViewWidget::mousePressEvent(QMouseEvent *event)
        assert(event);
 
        if (event->button() & Qt::LeftButton) {
+               if (event->modifiers() & Qt::ShiftModifier)
+                       view_.show_cursors(false);
+
                mouse_down_point_ = event->pos();
+               mouse_down_offset_ = view_.offset() + event->pos().x() * view_.scale();
                mouse_down_item_ = get_mouse_over_item(event->pos());
                mouse_left_press_event(event);
        }
@@ -280,33 +284,80 @@ void ViewWidget::mouseReleaseEvent(QMouseEvent *event)
        mouse_down_item_ = nullptr;
 }
 
+void ViewWidget::keyReleaseEvent(QKeyEvent *event)
+{
+       // Update mouse_modifiers_ also if modifiers change, but pointer doesn't move
+       if ((mouse_point_.x() >= 0) && (mouse_point_.y() >= 0)) // mouse is inside
+               mouse_modifiers_ = event->modifiers();
+       update();
+}
+
+void ViewWidget::keyPressEvent(QKeyEvent *event)
+{
+       // Update mouse_modifiers_ also if modifiers change, but pointer doesn't move
+       if ((mouse_point_.x() >= 0) && (mouse_point_.y() >= 0)) // mouse is inside
+               mouse_modifiers_ = event->modifiers();
+       update();
+}
+
 void ViewWidget::mouseMoveEvent(QMouseEvent *event)
 {
        assert(event);
        mouse_point_ = event->pos();
+       mouse_modifiers_ = event->modifiers();
 
        if (!event->buttons())
                item_hover(get_mouse_over_item(event->pos()), event->pos());
-       else if (event->buttons() & Qt::LeftButton) {
-               if (!item_dragging_) {
-                       if ((event->pos() - mouse_down_point_).manhattanLength() <
-                               QApplication::startDragDistance())
-                               return;
 
-                       if (!accept_drag())
-                               return;
+       if (event->buttons() & Qt::LeftButton) {
+               if (event->modifiers() & Qt::ShiftModifier) {
+                       // Cursor drag
+                       pv::util::Timestamp current_offset = view_.offset() + event->pos().x() * view_.scale();
 
-                       item_dragging_ = true;
-               }
+                       const int drag_distance = qAbs(current_offset.convert_to<double>() -
+                               mouse_down_offset_.convert_to<double>()) / view_.scale();
+
+                       if (drag_distance > QApplication::startDragDistance()) {
+                               view_.show_cursors(true);
+                               view_.set_cursors(mouse_down_offset_, current_offset);
+                       } else
+                               view_.show_cursors(false);
 
-               // Do the drag
-               drag_items(event->pos() - mouse_down_point_);
+               } else {
+                       if (!item_dragging_) {
+                               if ((event->pos() - mouse_down_point_).manhattanLength() <
+                                       QApplication::startDragDistance())
+                                       return;
+
+                               if (!accept_drag())
+                                       return;
+
+                               item_dragging_ = true;
+                       }
+
+                       // Do the drag
+                       drag_items(event->pos() - mouse_down_point_);
+               }
        }
+
+       // Force a repaint of the widget to update highlighted parts
+       update();
 }
 
 void ViewWidget::leaveEvent(QEvent*)
 {
-       mouse_point_ = QPoint(-1, -1);
+       bool cursor_above_widget = rect().contains(mapFromGlobal(QCursor::pos()));
+
+       // We receive leaveEvent also when the widget loses focus even when
+       // the mouse cursor hasn't moved at all - e.g. when the popup shows.
+       // However, we don't want to reset mouse_position_ when the mouse is
+       // still above this widget as doing so would break the context menu
+       if (!cursor_above_widget)
+               mouse_point_ = QPoint(INT_MIN, INT_MIN);
+
+       mouse_modifiers_ = Qt::NoModifier;
+       item_hover(nullptr, QPoint());
+
        update();
 }
 
index f4928e67f885bcd948c10ae0b72a7da03bff5d5b..427a8994e60cbeadf91995ce5fc6c3bfda76137e 100644 (file)
@@ -25,6 +25,8 @@
 #include <QPoint>
 #include <QWidget>
 
+#include <pv/util.hpp>
+
 using std::shared_ptr;
 using std::vector;
 
@@ -131,6 +133,9 @@ protected:
        void mouseReleaseEvent(QMouseEvent *event);
        void mouseMoveEvent(QMouseEvent *event);
 
+       void keyPressEvent(QKeyEvent *event);
+       void keyReleaseEvent(QKeyEvent *event);
+
        void leaveEvent(QEvent *event);
 
 public Q_SLOTS:
@@ -143,7 +148,12 @@ protected:
        pv::views::trace::View &view_;
        QPoint mouse_point_;
        QPoint mouse_down_point_;
+       pv::util::Timestamp mouse_down_offset_;
        shared_ptr<ViewItem> mouse_down_item_;
+
+       /// Keyboard modifiers that were active when mouse was last moved or clicked
+       Qt::KeyboardModifiers mouse_modifiers_;
+
        bool item_dragging_;
 };
 
index 3c9bc8c0eebc045b034eb8088464f5a4e98d5547..24e4bb9a8816b2dcf945907f297637b4d8e1ea07 100644 (file)
@@ -33,10 +33,18 @@ using std::shared_ptr;
 namespace pv {
 namespace views {
 
+const char* ViewTypeNames[ViewTypeCount] = {
+       "Trace View",
+#ifdef ENABLE_DECODE
+       "Binary Decoder Output View"
+#endif
+};
+
 const int ViewBase::MaxViewAutoUpdateRate = 25; // No more than 25 Hz
 
-ViewBase::ViewBase(Session &session, bool is_main_view, QWidget *parent) :
+ViewBase::ViewBase(Session &session, bool is_main_view, QMainWindow *parent) :
        // Note: Place defaults in ViewBase::reset_view_state(), not here
+       QWidget(parent),
        session_(session),
        is_main_view_(is_main_view)
 {
@@ -55,9 +63,13 @@ ViewBase::ViewBase(Session &session, bool is_main_view, QWidget *parent) :
        delayed_view_updater_.setInterval(1000 / MaxViewAutoUpdateRate);
 }
 
+bool ViewBase::is_main_view() const
+{
+       return is_main_view_;
+}
+
 void ViewBase::reset_view_state()
 {
-       ruler_shift_ = 0;
        current_segment_ = 0;
 }
 
@@ -73,9 +85,10 @@ const Session& ViewBase::session() const
 
 void ViewBase::clear_signals()
 {
+       clear_signalbases();
 }
 
-unordered_set< shared_ptr<data::SignalBase> > ViewBase::signalbases() const
+vector< shared_ptr<data::SignalBase> > ViewBase::signalbases() const
 {
        return signalbases_;
 }
@@ -94,7 +107,7 @@ void ViewBase::clear_signalbases()
 
 void ViewBase::add_signalbase(const shared_ptr<data::SignalBase> signalbase)
 {
-       signalbases_.insert(signalbase);
+       signalbases_.push_back(signalbase);
 
        connect(signalbase.get(), SIGNAL(samples_cleared()),
                this, SLOT(on_data_updated()));
@@ -105,16 +118,20 @@ void ViewBase::add_signalbase(const shared_ptr<data::SignalBase> signalbase)
 #ifdef ENABLE_DECODE
 void ViewBase::clear_decode_signals()
 {
+       decode_signals_.clear();
 }
 
 void ViewBase::add_decode_signal(shared_ptr<data::DecodeSignal> signal)
 {
-       (void)signal;
+       decode_signals_.push_back(signal);
 }
 
 void ViewBase::remove_decode_signal(shared_ptr<data::DecodeSignal> signal)
 {
-       (void)signal;
+       decode_signals_.erase(std::remove_if(
+               decode_signals_.begin(), decode_signals_.end(),
+               [&](shared_ptr<data::DecodeSignal> s) { return s == signal; }),
+               decode_signals_.end());
 }
 #endif
 
index b524c1797d0d1478ba4bd1eac36ee538a400d69a..585dfa0c91ffd6e204c4cfe203e2de5846881834 100644 (file)
@@ -26,6 +26,7 @@
 #include <unordered_set>
 #include <vector>
 
+#include <QMainWindow>
 #include <QTimer>
 #include <QWidget>
 
@@ -37,7 +38,7 @@
 #endif
 
 using std::shared_ptr;
-using std::unordered_set;
+using std::vector;
 
 namespace pv {
 
@@ -50,20 +51,29 @@ class Signal;
 
 namespace views {
 
+// When adding an entry here, don't forget to update ViewTypeNames as well
 enum ViewType {
        ViewTypeTrace,
-       ViewTypeTabularDecode
+#ifdef ENABLE_DECODE
+       ViewTypeDecoderBinary,
+#endif
+       ViewTypeCount  // Indicates how many view types there are, must always be last
 };
 
+extern const char* ViewTypeNames[ViewTypeCount];
+
 class ViewBase : public QWidget
 {
        Q_OBJECT
 
-private:
+public:
        static const int MaxViewAutoUpdateRate;
 
 public:
-       explicit ViewBase(Session &session, bool is_main_view = false, QWidget *parent = nullptr);
+       explicit ViewBase(Session &session, bool is_main_view = false, QMainWindow *parent = nullptr);
+
+       virtual ViewType get_type() const = 0;
+       bool is_main_view() const;
 
        /**
         * Resets the view to its default state after construction. It does however
@@ -79,7 +89,7 @@ public:
        /**
         * Returns the signal bases contained in this view.
         */
-       unordered_set< shared_ptr<data::SignalBase> > signalbases() const;
+       vector< shared_ptr<data::SignalBase> > signalbases() const;
 
        virtual void clear_signalbases();
 
@@ -116,10 +126,12 @@ protected:
 
        const bool is_main_view_;
 
-       util::Timestamp ruler_shift_;
        util::TimeUnit time_unit_;
 
-       unordered_set< shared_ptr<data::SignalBase> > signalbases_;
+       vector< shared_ptr<data::SignalBase> > signalbases_;
+#ifdef ENABLE_DECODE
+       vector< shared_ptr<data::DecodeSignal> > decode_signals_;
+#endif
 
        /// The ID of the currently displayed segment
        uint32_t current_segment_;
index 28b54babdfb9aad3c0e67e2a732b22ac79cbe918..505a1f67e9e4179c756f2ae33f148cf793686738 100644 (file)
 namespace pv {
 namespace widgets {
 
-DecoderMenu::DecoderMenu(QWidget *parent, bool first_level_decoder) :
+DecoderMenu::DecoderMenu(QWidget *parent, const char* input, bool first_level_decoder) :
        QMenu(parent),
        mapper_(this)
 {
-       GSList *li = g_slist_sort(g_slist_copy(
-               (GSList*)srd_decoder_list()), decoder_name_cmp);
+       GSList *li = g_slist_sort(g_slist_copy((GSList*)srd_decoder_list()), decoder_name_cmp);
+
        for (GSList *l = li; l; l = l->next) {
                const srd_decoder *const d = (srd_decoder*)l->data;
                assert(d);
 
                const bool have_channels = (d->channels || d->opt_channels) != 0;
-               if (first_level_decoder == have_channels) {
-                       QAction *const action =
-                               addAction(QString::fromUtf8(d->name));
-                       action->setData(qVariantFromValue(l->data));
-                       mapper_.setMapping(action, action);
-                       connect(action, SIGNAL(triggered()),
-                               &mapper_, SLOT(map()));
+               if (first_level_decoder != have_channels)
+                       continue;
+
+               if (!first_level_decoder) {
+                       // Dismiss all non-stacked decoders unless we're looking for first-level decoders
+                       if (!d->inputs)
+                               continue;
+
+                       // TODO For now we ignore that d->inputs is actually a list
+                       if (strncmp((char*)(d->inputs->data), input, 1024) != 0)
+                               continue;
                }
+
+               QAction *const action = addAction(QString::fromUtf8(d->name));
+               action->setData(qVariantFromValue(l->data));
+               mapper_.setMapping(action, action);
+               connect(action, SIGNAL(triggered()), &mapper_, SLOT(map()));
        }
        g_slist_free(li);
 
-       connect(&mapper_, SIGNAL(mapped(QObject*)),
-               this, SLOT(on_action(QObject*)));
+       connect(&mapper_, SIGNAL(mapped(QObject*)), this, SLOT(on_action(QObject*)));
 }
 
 int DecoderMenu::decoder_name_cmp(const void *a, const void *b)
 {
-       return strcmp(((const srd_decoder*)a)->name,
-               ((const srd_decoder*)b)->name);
+       return strcmp(((const srd_decoder*)a)->name, ((const srd_decoder*)b)->name);
 }
 
 void DecoderMenu::on_action(QObject *action)
 {
        assert(action);
-       srd_decoder *const dec =
-               (srd_decoder*)((QAction*)action)->data().value<void*>();
+
+       srd_decoder *const dec = (srd_decoder*)((QAction*)action)->data().value<void*>();
        assert(dec);
 
        decoder_selected(dec);
index 2fe7cd2b28e9e1c68f0ab093e456a44f80ed2c11..6ef4d2c6bdffcde8827a550069b5b34484b8c788 100644 (file)
@@ -33,7 +33,7 @@ class DecoderMenu : public QMenu
        Q_OBJECT;
 
 public:
-       DecoderMenu(QWidget *parent, bool first_level_decoder = false);
+       DecoderMenu(QWidget *parent, const char* input, bool first_level_decoder = false);
 
 private:
        static int decoder_name_cmp(const void *a, const void *b);
diff --git a/pv/widgets/flowlayout.cpp b/pv/widgets/flowlayout.cpp
new file mode 100644 (file)
index 0000000..efd862f
--- /dev/null
@@ -0,0 +1,212 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2015 The Qt Company Ltd.
+ ** Contact: http://www.qt.io/licensing/
+ **
+ ** This file is part of the examples of the Qt Toolkit.
+ **
+ ** $QT_BEGIN_LICENSE:BSD$
+ ** You may use this file under the terms of the BSD license as follows:
+ **
+ ** "Redistribution and use in source and binary forms, with or without
+ ** modification, are permitted provided that the following conditions are
+ ** met:
+ **   * Redistributions of source code must retain the above copyright
+ **     notice, this list of conditions and the following disclaimer.
+ **   * Redistributions in binary form must reproduce the above copyright
+ **     notice, this list of conditions and the following disclaimer in
+ **     the documentation and/or other materials provided with the
+ **     distribution.
+ **   * Neither the name of The Qt Company Ltd nor the names of its
+ **     contributors may be used to endorse or promote products derived
+ **     from this software without specific prior written permission.
+ **
+ **
+ ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+ **
+ ** $QT_END_LICENSE$
+ **
+ ****************************************************************************/
+
+#include <QWidget>
+
+#include "flowlayout.hpp"
+
+FlowLayout::FlowLayout(QWidget *parent, int margin, int hSpacing, int vSpacing) :
+       QLayout(parent),
+       m_parent(parent),
+       m_hSpace(hSpacing),
+       m_vSpace(vSpacing)
+{
+       setContentsMargins(margin, margin, margin, margin);
+}
+
+FlowLayout::FlowLayout(int margin, int hSpacing, int vSpacing) :
+       m_parent(nullptr),
+       m_hSpace(hSpacing),
+       m_vSpace(vSpacing)
+{
+       setContentsMargins(margin, margin, margin, margin);
+}
+
+FlowLayout::~FlowLayout()
+{
+       QLayoutItem *item;
+       while ((item = takeAt(0)))
+               delete item;
+}
+
+void FlowLayout::addItem(QLayoutItem *item)
+{
+       itemList.append(item);
+}
+
+int FlowLayout::horizontalSpacing() const
+{
+       if (m_hSpace >= 0)
+               return m_hSpace;
+       else
+               return smartSpacing(QStyle::PM_LayoutHorizontalSpacing);
+}
+
+int FlowLayout::verticalSpacing() const
+{
+       if (m_vSpace >= 0)
+               return m_vSpace;
+       else
+               return smartSpacing(QStyle::PM_LayoutVerticalSpacing);
+}
+
+int FlowLayout::count() const
+{
+       return itemList.size();
+}
+
+QLayoutItem *FlowLayout::itemAt(int index) const
+{
+       return itemList.value(index);
+}
+
+QLayoutItem *FlowLayout::takeAt(int index)
+{
+       if ((index >= 0) && (index < itemList.size()))
+               return itemList.takeAt(index);
+       else
+               return nullptr;
+}
+
+Qt::Orientations FlowLayout::expandingDirections() const
+{
+       return Qt::Horizontal | Qt::Vertical;
+}
+
+bool FlowLayout::hasHeightForWidth() const
+{
+       return true;
+}
+
+int FlowLayout::heightForWidth(int width) const
+{
+       int height = doLayout(QRect(0, 0, width, 0), true);
+       return height;
+}
+
+void FlowLayout::setGeometry(const QRect &rect)
+{
+       QLayout::setGeometry(rect);
+       doLayout(rect, false);
+}
+
+QSize FlowLayout::sizeHint() const
+{
+       return minimumSize();
+}
+
+QSize FlowLayout::minimumSize() const
+{
+       QSize size(0, 0);
+
+       for (QLayoutItem* item : itemList) {
+               int w = item->geometry().x() + item->geometry().width();
+               if (w > size.width())
+                       size.setWidth(w);
+
+               int h = item->geometry().y() + item->geometry().height();
+               if (h > size.height())
+                       size.setHeight(h);
+       }
+
+       size += QSize(2 * margin(), 2 * margin());
+
+       return size;
+}
+
+int FlowLayout::doLayout(const QRect &rect, bool testOnly) const
+{
+       int left, top, right, bottom;
+       getContentsMargins(&left, &top, &right, &bottom);
+
+       QRect effectiveRect = rect.adjusted(left, top, -right, -bottom);
+       int x = effectiveRect.x();
+       int y = effectiveRect.y();
+
+       int lineHeight = 0;
+       for (QLayoutItem* item : itemList) {
+               QWidget* w = item->widget();
+
+               int spaceX = horizontalSpacing();
+               if (spaceX == -1)
+                       spaceX = w->style()->layoutSpacing(
+                               QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Horizontal);
+
+               int spaceY = verticalSpacing();
+               if (spaceY == -1)
+                       spaceY = w->style()->layoutSpacing(
+                               QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Vertical);
+
+               int nextX = x + item->sizeHint().width() + spaceX;
+               if (((nextX - spaceX) > effectiveRect.right()) && (lineHeight > 0)) {
+                       x = effectiveRect.x();
+                       y = y + lineHeight + spaceY;
+                       nextX = x + item->sizeHint().width() + spaceX;
+                       lineHeight = 0;
+               }
+
+               if (!testOnly)
+                       item->setGeometry(QRect(QPoint(x, y), item->sizeHint()));
+
+               x = nextX;
+               lineHeight = qMax(lineHeight, item->sizeHint().height());
+       }
+
+       int height = y + lineHeight - rect.y() + bottom;
+
+       if (m_parent)
+               m_parent->setMinimumHeight(height);
+
+       return height;
+}
+
+int FlowLayout::smartSpacing(QStyle::PixelMetric pm) const
+{
+       QObject *parent = this->parent();
+
+       if (!parent)
+               return -1;
+
+       if (parent->isWidgetType()) {
+               QWidget *pw = qobject_cast<QWidget*>(parent);
+               return pw->style()->pixelMetric(pm, nullptr, pw);
+       } else
+               return static_cast<QLayout*>(parent)->spacing();
+}
diff --git a/pv/widgets/flowlayout.hpp b/pv/widgets/flowlayout.hpp
new file mode 100644 (file)
index 0000000..81407d1
--- /dev/null
@@ -0,0 +1,78 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2015 The Qt Company Ltd.
+ ** Contact: http://www.qt.io/licensing/
+ **
+ ** This file is part of the examples of the Qt Toolkit.
+ **
+ ** $QT_BEGIN_LICENSE:BSD$
+ ** You may use this file under the terms of the BSD license as follows:
+ **
+ ** "Redistribution and use in source and binary forms, with or without
+ ** modification, are permitted provided that the following conditions are
+ ** met:
+ **   * Redistributions of source code must retain the above copyright
+ **     notice, this list of conditions and the following disclaimer.
+ **   * Redistributions in binary form must reproduce the above copyright
+ **     notice, this list of conditions and the following disclaimer in
+ **     the documentation and/or other materials provided with the
+ **     distribution.
+ **   * Neither the name of The Qt Company Ltd nor the names of its
+ **     contributors may be used to endorse or promote products derived
+ **     from this software without specific prior written permission.
+ **
+ **
+ ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+ **
+ ** $QT_END_LICENSE$
+ **
+ ****************************************************************************/
+
+#ifndef FLOWLAYOUT_H
+#define FLOWLAYOUT_H
+
+#include <QLayout>
+#include <QRect>
+#include <QStyle>
+#include <QWidgetItem>
+
+class FlowLayout : public QLayout
+{
+public:
+       FlowLayout(QWidget *parent, int margin = -1, int hSpacing = -1, int vSpacing = -1);
+       FlowLayout(int margin = -1, int hSpacing = -1, int vSpacing = -1);
+       ~FlowLayout();
+
+       void addItem(QLayoutItem *item);
+       int horizontalSpacing() const;
+       int verticalSpacing() const;
+       Qt::Orientations expandingDirections() const;
+       bool hasHeightForWidth() const;
+       int heightForWidth(int) const;
+       int count() const;
+       QLayoutItem *itemAt(int index) const;
+       QSize minimumSize() const;
+       void setGeometry(const QRect &rect);
+       QSize sizeHint() const;
+       QLayoutItem *takeAt(int index);
+
+private:
+       int doLayout(const QRect &rect, bool testOnly) const;
+       int smartSpacing(QStyle::PixelMetric pm) const;
+
+       QWidget* m_parent;
+       QList<QLayoutItem*> itemList;
+       int m_hSpace, m_vSpace;
+};
+
+#endif
index 1a45aa6207dec2cd493719bfa86f5eb376a80afb..e3034249769e66c73041b37af30fcab6f0ec1392 100644 (file)
@@ -31,6 +31,7 @@ using std::map;
 using std::pair;
 using std::string;
 using std::shared_ptr;
+using std::vector;
 
 using sigrok::Context;
 using sigrok::InputFormat;
@@ -39,16 +40,23 @@ namespace pv {
 namespace widgets {
 
 ImportMenu::ImportMenu(QWidget *parent, shared_ptr<Context> context,
-       QAction *open_action) :
+       vector<QAction *>open_actions) :
        QMenu(parent),
        context_(context),
        mapper_(this)
 {
        assert(context);
 
-       if (open_action) {
-               addAction(open_action);
-               setDefaultAction(open_action);
+       if (!open_actions.empty()) {
+               bool first_action = true;
+               for (auto open_action : open_actions) {
+                       addAction(open_action);
+
+                       if (first_action) {
+                               first_action = false;
+                               setDefaultAction(open_action);
+                       }
+               }
                addSeparator();
        }
 
index d1d5231afd33d5832c58179f1becb80cca38a4e8..c70962a4a781c4fef7fed32857bfce4a6787a749 100644 (file)
@@ -26,6 +26,7 @@
 #include <QSignalMapper>
 
 using std::shared_ptr;
+using std::vector;
 
 namespace sigrok {
 class Context;
@@ -41,7 +42,7 @@ class ImportMenu : public QMenu
 
 public:
        ImportMenu(QWidget *parent, shared_ptr<sigrok::Context> context,
-               QAction *open_action = nullptr);
+               vector<QAction *>open_actions = vector<QAction *>());
 
 private Q_SLOTS:
        void on_action(QObject *action);
index 13282cda25e458b6b624de563f09e1fc6668a6b5..ec6d29c981c235b86451353ca620d9c5cf147868 100644 (file)
@@ -23,6 +23,8 @@
 #include <QApplication>
 #include <QDesktopWidget>
 #include <QLineEdit>
+#include <QScrollBar>
+#include <QStyle>
 #include <QtGui>
 
 #include "popup.hpp"
@@ -37,6 +39,33 @@ const unsigned int Popup::ArrowLength = 10;
 const unsigned int Popup::ArrowOverlap = 3;
 const unsigned int Popup::MarginWidth = 6;
 
+
+QWidthAdjustingScrollArea::QWidthAdjustingScrollArea(QWidget* parent) :
+       QScrollArea(parent)
+{
+}
+
+void QWidthAdjustingScrollArea::setWidget(QWidget* w)
+{
+       QScrollArea::setWidget(w);
+       // It happens that QScrollArea already filters widget events,
+       // but that's an implementation detail that we shouldn't rely on.
+       w->installEventFilter(this);
+}
+
+bool QWidthAdjustingScrollArea::eventFilter(QObject* obj, QEvent* ev)
+{
+       if (obj == widget() && ev->type() == QEvent::Resize) {
+               if (widget()->height() > height())
+                       setMinimumWidth(widget()->width() + qApp->style()->pixelMetric(QStyle::PM_ScrollBarExtent));
+               else
+                       setMinimumWidth(widget()->width());
+       }
+
+       return QScrollArea::eventFilter(obj, ev);
+}
+
+
 Popup::Popup(QWidget *parent) :
        QWidget(parent, Qt::Popup | Qt::FramelessWindowHint),
        point_(),
index 66f7f06f5d87192a2ea77cbafee44af21cec294c..879c5f9d83542e3a4fc50485441554cabdc9ea24 100644 (file)
 #ifndef PULSEVIEW_PV_WIDGETS_POPUP_HPP
 #define PULSEVIEW_PV_WIDGETS_POPUP_HPP
 
+#include <QScrollArea>
 #include <QWidget>
 
 namespace pv {
 namespace widgets {
 
+
+// A regular QScrollArea has a fixed size and provides scroll bars when the
+// content can't be shown in its entirety. However, we want no horizontal
+// scroll bar and want the scroll area to adjust its width to fit the content
+// instead.
+// Inspired by https://stackoverflow.com/questions/21253755/qscrollarea-with-dynamically-changing-contents?answertab=votes#tab-top
+class QWidthAdjustingScrollArea : public QScrollArea
+{
+       Q_OBJECT
+
+public:
+       QWidthAdjustingScrollArea(QWidget* parent = nullptr);
+       void setWidget(QWidget* w);
+       bool eventFilter(QObject* obj, QEvent* ev);
+};
+
+
 class Popup : public QWidget
 {
        Q_OBJECT
index 21b3d0d7a50c1610a3365bcabbe16730dcca696b..fea8175e8e00b74b21f6690cf9d726e3a9cd418c 100644 (file)
@@ -31,6 +31,7 @@ TimestampSpinBox::TimestampSpinBox(QWidget* parent)
        , stepsize_("1e-6")
 {
        connect(this, SIGNAL(editingFinished()), this, SLOT(on_editingFinished()));
+       connect(lineEdit(), SIGNAL(editingFinished()), this, SLOT(on_editingFinished()));
 
        updateEdit();
 }
@@ -92,10 +93,6 @@ void TimestampSpinBox::setValue(const pv::util::Timestamp& val)
 
 void TimestampSpinBox::on_editingFinished()
 {
-       if (!lineEdit()->isModified())
-               return;
-       lineEdit()->setModified(false);
-
        QRegExp re(R"(\s*([-+]?)\s*([0-9]+\.?[0-9]*).*)");
 
        if (re.exactMatch(text())) {
@@ -103,6 +100,7 @@ void TimestampSpinBox::on_editingFinished()
                captures.removeFirst(); // remove entire match
                QString str = captures.join("");
                setValue(pv::util::Timestamp(str.toStdString()));
+
        } else {
                // replace the malformed entered string with the old value
                updateEdit();
@@ -113,7 +111,11 @@ void TimestampSpinBox::updateEdit()
 {
        QString newtext = pv::util::format_time_si(
                value_, pv::util::SIPrefix::none, precision_);
+       const QSignalBlocker blocker(lineEdit());
+       // Keep cursor position
+       int cursor = lineEdit()->cursorPosition();
        lineEdit()->setText(newtext);
+       lineEdit()->setCursorPosition(cursor);
 }
 
 }  // namespace widgets
index 4c67ca114d0f4ba236a4428bab30a7fcbdfa61bb..8b2e1ef2759b058d6ea647916fc3bd4c136c70d0 100644 (file)
@@ -52,8 +52,8 @@ struct WellArrayData;
 class WellArray : public QWidget
 {
     Q_OBJECT
-    Q_PROPERTY(int selectedColumn READ selectedColumn)  // clazy-exclude:qproperty-without-notify
-    Q_PROPERTY(int selectedRow READ selectedRow)  // clazy-exclude:qproperty-without-notify
+    Q_PROPERTY(int selectedColumn READ selectedColumn)  // clazy:exclude=qproperty-without-notify
+    Q_PROPERTY(int selectedRow READ selectedRow)  // clazy:exclude=qproperty-without-notify
 
 public:
     WellArray(int rows, int cols, QWidget* parent = nullptr);
index 300532e11394744e407b124feee4e903fed22ec1..e3362ac8ef961d1ce7da45568acef3510b8097a3 100644 (file)
@@ -54,6 +54,7 @@ set(pulseview_TEST_SOURCES
        ${PROJECT_SOURCE_DIR}/pv/prop/string.cpp
        ${PROJECT_SOURCE_DIR}/pv/popups/channels.cpp
        ${PROJECT_SOURCE_DIR}/pv/popups/deviceoptions.cpp
+       ${PROJECT_SOURCE_DIR}/pv/subwindows/subwindowbase.cpp
        ${PROJECT_SOURCE_DIR}/pv/toolbars/mainbar.cpp
        ${PROJECT_SOURCE_DIR}/pv/views/trace/analogsignal.cpp
        ${PROJECT_SOURCE_DIR}/pv/views/trace/cursor.cpp
@@ -62,7 +63,6 @@ set(pulseview_TEST_SOURCES
        ${PROJECT_SOURCE_DIR}/pv/views/trace/header.cpp
        ${PROJECT_SOURCE_DIR}/pv/views/trace/marginwidget.cpp
        ${PROJECT_SOURCE_DIR}/pv/views/trace/logicsignal.cpp
-       ${PROJECT_SOURCE_DIR}/pv/views/trace/rowitem.cpp
        ${PROJECT_SOURCE_DIR}/pv/views/trace/ruler.cpp
        ${PROJECT_SOURCE_DIR}/pv/views/trace/signal.cpp
        ${PROJECT_SOURCE_DIR}/pv/views/trace/timeitem.cpp
@@ -85,6 +85,7 @@ set(pulseview_TEST_SOURCES
        ${PROJECT_SOURCE_DIR}/pv/widgets/colorpopup.cpp
        ${PROJECT_SOURCE_DIR}/pv/widgets/devicetoolbutton.cpp
        ${PROJECT_SOURCE_DIR}/pv/widgets/exportmenu.cpp
+       ${PROJECT_SOURCE_DIR}/pv/widgets/flowlayout.cpp
        ${PROJECT_SOURCE_DIR}/pv/widgets/importmenu.cpp
        ${PROJECT_SOURCE_DIR}/pv/widgets/popup.cpp
        ${PROJECT_SOURCE_DIR}/pv/widgets/popuptoolbutton.cpp
@@ -127,6 +128,7 @@ set(pulseview_TEST_HEADERS
        ${PROJECT_SOURCE_DIR}/pv/prop/int.hpp
        ${PROJECT_SOURCE_DIR}/pv/prop/property.hpp
        ${PROJECT_SOURCE_DIR}/pv/prop/string.hpp
+       ${PROJECT_SOURCE_DIR}/pv/subwindows/subwindowbase.hpp
        ${PROJECT_SOURCE_DIR}/pv/toolbars/mainbar.hpp
        ${PROJECT_SOURCE_DIR}/pv/views/trace/analogsignal.hpp
        ${PROJECT_SOURCE_DIR}/pv/views/trace/cursor.hpp
@@ -134,7 +136,6 @@ set(pulseview_TEST_HEADERS
        ${PROJECT_SOURCE_DIR}/pv/views/trace/header.hpp
        ${PROJECT_SOURCE_DIR}/pv/views/trace/logicsignal.hpp
        ${PROJECT_SOURCE_DIR}/pv/views/trace/marginwidget.hpp
-       ${PROJECT_SOURCE_DIR}/pv/views/trace/rowitem.hpp
        ${PROJECT_SOURCE_DIR}/pv/views/trace/ruler.hpp
        ${PROJECT_SOURCE_DIR}/pv/views/trace/signal.hpp
        ${PROJECT_SOURCE_DIR}/pv/views/trace/timeitem.hpp
@@ -153,6 +154,7 @@ set(pulseview_TEST_HEADERS
        ${PROJECT_SOURCE_DIR}/pv/widgets/colorpopup.hpp
        ${PROJECT_SOURCE_DIR}/pv/widgets/devicetoolbutton.hpp
        ${PROJECT_SOURCE_DIR}/pv/widgets/exportmenu.hpp
+       ${PROJECT_SOURCE_DIR}/pv/widgets/flowlayout.hpp
        ${PROJECT_SOURCE_DIR}/pv/widgets/importmenu.hpp
        ${PROJECT_SOURCE_DIR}/pv/widgets/popup.hpp
        ${PROJECT_SOURCE_DIR}/pv/widgets/popuptoolbutton.hpp
@@ -169,6 +171,11 @@ if(ENABLE_DECODE)
                ${PROJECT_SOURCE_DIR}/pv/data/decode/decoder.cpp
                ${PROJECT_SOURCE_DIR}/pv/data/decode/row.cpp
                ${PROJECT_SOURCE_DIR}/pv/data/decode/rowdata.cpp
+               ${PROJECT_SOURCE_DIR}/pv/subwindows/decoder_selector/item.cpp
+               ${PROJECT_SOURCE_DIR}/pv/subwindows/decoder_selector/model.cpp
+               ${PROJECT_SOURCE_DIR}/pv/subwindows/decoder_selector/subwindow.cpp
+               ${PROJECT_SOURCE_DIR}/pv/views/decoder_binary/view.cpp
+               ${PROJECT_SOURCE_DIR}/pv/views/decoder_binary/QHexView.cpp
                ${PROJECT_SOURCE_DIR}/pv/views/trace/decodetrace.cpp
                ${PROJECT_SOURCE_DIR}/pv/widgets/decodergroupbox.cpp
                ${PROJECT_SOURCE_DIR}/pv/widgets/decodermenu.cpp
@@ -176,6 +183,9 @@ if(ENABLE_DECODE)
 
        list(APPEND pulseview_TEST_HEADERS
                ${PROJECT_SOURCE_DIR}/pv/data/decodesignal.hpp
+               ${PROJECT_SOURCE_DIR}/pv/subwindows/decoder_selector/subwindow.hpp
+               ${PROJECT_SOURCE_DIR}/pv/views/decoder_binary/view.hpp
+               ${PROJECT_SOURCE_DIR}/pv/views/decoder_binary/QHexView.hpp
                ${PROJECT_SOURCE_DIR}/pv/views/trace/decodetrace.hpp
                ${PROJECT_SOURCE_DIR}/pv/widgets/decodergroupbox.hpp
                ${PROJECT_SOURCE_DIR}/pv/widgets/decodermenu.hpp
diff --git a/translations.qrc b/translations.qrc
new file mode 100644 (file)
index 0000000..103fa35
--- /dev/null
@@ -0,0 +1,5 @@
+<RCC>
+       <qresource prefix="/">
+               <file>l10n/de.qm</file>
+       </qresource>
+</RCC>