From: Uwe Hermann Date: Tue, 31 Mar 2020 20:35:18 +0000 (+0200) Subject: Backport recent changes from mainline. X-Git-Tag: pulseview-0.4.2~4 X-Git-Url: https://sigrok.org/gitweb/?p=pulseview.git;a=commitdiff_plain;h=10f5e6f54e0c36582e846a769b67b1e7e5a371f8;ds=sidebyside Backport recent changes from mainline. 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 --- diff --git a/CMakeLists.txt b/CMakeLists.txt index a19593d7..79d3a69c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,7 @@ ## ## Copyright (C) 2012 Joel Holdsworth ## Copyright (C) 2012-2013 Alexandru Gagniuc +## Copyright (C) 2020 Soeren Apel ## ## This 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 edb7940d..d666f0a7 100644 --- 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 8608b3bf..a35cb8d5 100644 --- 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 ------------ diff --git a/contrib/org.sigrok.PulseView.appdata.xml b/contrib/org.sigrok.PulseView.appdata.xml index 48a53971..5097897f 100644 --- a/contrib/org.sigrok.PulseView.appdata.xml +++ b/contrib/org.sigrok.PulseView.appdata.xml @@ -7,20 +7,19 @@ PulseView Logic analyzer, oscilloscope and MSO GUI -

PulseView is a Qt based logic analyzer, oscilloscope and MSO GUI for sigrok.

-

Features:

+

PulseView is a Qt based logic analyzer, oscilloscope and MSO GUI for sigrok supporting various features:

  • Fast O(log N) signal rendering at all zoom levels
  • Protocol decoder support
  • Trace groups support
- http://sigrok.org/wiki/PulseView - http://sigrok.org/bugzilla/enter_bug.cgi?format=guided&product=PulseView - http://sigrok.org/wiki/FAQ + https://sigrok.org/wiki/PulseView + https://sigrok.org/bugzilla/enter_bug.cgi?format=guided&product=PulseView + https://sigrok.org/wiki/FAQ - http://sigrok.org/wimg/e/ee/PulseView_I2C_DS1307_Decode.png + https://sigrok.org/wimg/e/ee/PulseView_I2C_DS1307_Decode.png diff --git a/contrib/pulseview_cross.nsi.in b/contrib/pulseview_cross.nsi.in index fdcdf49e..88945f44 100644 --- a/contrib/pulseview_cross.nsi.in +++ b/contrib/pulseview_cross.nsi.in @@ -1,7 +1,7 @@ ## ## This file is part of the PulseView project. ## -## Copyright (C) 2013-2014 Uwe Hermann +## Copyright (C) 2013-2020 Uwe Hermann ## ## This 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. diff --git a/doc/pulseview.1 b/doc/pulseview.1 index 8289eaa8..9663fdf9 100644 --- a/doc/pulseview.1 +++ b/doc/pulseview.1 @@ -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 " Specifies the format of the input file to be loaded. .TP +.BR "\-s, \-\-settings " +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 index 00000000..39150d71 --- /dev/null +++ b/icons/edit-paste.svg @@ -0,0 +1,531 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Edit Paste + 2005-10-10 + + + Andreas Nilsson + + + + + edit + paste + + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/search.svg b/icons/search.svg new file mode 100644 index 00000000..1a4c1cd7 --- /dev/null +++ b/icons/search.svg @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + diff --git a/l10n/de.ts b/l10n/de.ts new file mode 100644 index 00000000..94c09015 --- /dev/null +++ b/l10n/de.ts @@ -0,0 +1,1598 @@ + + + + + Application + + + Some parts of the application may still use the previous language. Re-opening the affected windows or restarting the application will remedy this. + 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. + + + + QApplication + + Select a decoder to see its description here. + Wähle einen Dekoder, um dessen Beschreibung hier lesen zu können. + + + Session %1 + Analysesitzung %1 + + + + Querying config key %1 is not allowed + Internal message + + + + + Querying config key %1 resulted in %2 + Internal message + + + + + Unknown type supplied when attempting to query %1 + Internal message + + + + + Error when scanning device driver '%1': %2 + Internal message + + + + + QHexView + + + No data available + Keine Daten vorhanden + + + + QObject + + + Cancel + Abbrechen + + + + Scanning for devices that driver %1 can access... + Suche nach Geräten, die von Treiber %1 angesprochen werden können... + + + + Stack trace of previous crash: + Internal message + + + + + Don't show this message again + Diese Meldung in Zukunft nicht mehr anzeigen + + + + 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. + Internal message + + + + + SubWindow + + <p align='right'>Tags: %1</p> + <p align='right'>Stichworte: %1</p> + + + + pv::MainWindow + + + PulseView + Name + + + + + Decoder Selector + Protokolldekoder + + + + Session %1 + Analysesitzung %1 + + + + Create New Session + Neue Analysesitzung + + + + Start/Stop Acquisition + Datenerfassung starten/stoppen + + + + Settings + Einstellungen + + + + Reload + Neu laden + + + + + + Confirmation + Bestätigung + + + + There is unsaved data. Close anyway? + Es gibt noch ungespeicherte Daten. Trotzdem beenden? + + + + + Run + Starten + + + + Stop + Stoppen + + + + + This session contains unsaved data. Close it anyway? + Die Daten dieser Analysesitzung wurden nicht gespeichert. Trotzdem schließen? + + + + pv::Session + + + Failed to select device + Fehler beim Auswählen des Gerätes + + + + Failed to open device + Fehler beim Öffnen des Gerätes + + + + Error + Fehler + + + + Unexpected input format: %s + Unerwartetes Importformat: %s + + + + Failed to load %1 + Fehler beim Laden von %1 + + + + No active device set, can't start acquisition. + Kein Gerät aktiv, kann Datenerfassung nicht starten. + + + + No channels enabled. + Keine aktiven Kanäle vorhanden. + + + + Out of memory, acquisition stopped. + Nicht genügend Arbeitsspeicher vorhanden, Datenerfassung wurde gestoppt. + + + + Can't handle more than 64 logic channels. + Internal message + + + + + pv::StoreSession + + + Can't save logic channel without data. + Kann Logikkanal nicht speichern, da er keine Daten beinhaltet. + + + + Can't save analog channel without data. + Kann Analogkanal nicht speichern, da er keine Daten beinhaltet. + + + + No channels enabled. + Keine Kanäle aktiviert. + + + + Can't save range without sample data. + In dem gewählten Bereich befinden sich keine Daten zum Speichern. + + + + + Error while saving: + Fehler beim Speichern: + + + + pv::data::DecodeSignal + + + No decoders + Keine Protokolldekoder + + + + There are no channels assigned to this decoder + Dem Protokolldekoder sind keine Kanäle zugeordnet + + + + One or more required channels have not been specified + Mindestens ein notwendiger Kanal wurde noch nicht zugeordnet + + + + No input data + Keine Daten zum Auswerten vorhanden + + + + Decoder reported an error + Protokolldekoder meldet Fehler + + + + Failed to create decoder instance + Fehler beim Erzeugen des Protokolldekoders + + + + pv::data::SignalBase + + + Signal average + Durchschnittlicher Signalpegel + + + + 0.9V (for 1.8V CMOS) + 0.9V (für 1.8V CMOS) + + + + 1.8V (for 3.3V CMOS) + 1.8V (für 3.3V CMOS) + + + + 2.5V (for 5.0V CMOS) + 2.5V (für 5.0V CMOS) + + + + 1.5V (for TTL) + 1.5V (für TTL) + + + + Signal average +/- 15% + Durchschnittlicher Signalpegel +/- 15% + + + + 0.3V/1.2V (for 1.8V CMOS) + 0.3V/1.2V (für 1.8V CMOS) + + + + 0.7V/2.5V (for 3.3V CMOS) + 0.7V/2.5V (für 3.3V CMOS) + + + + 1.3V/3.7V (for 5.0V CMOS) + 1.3V/3.7V (für 5.0V CMOS) + + + + 0.8V/2.0V (for TTL) + 0.8V/2.0V (für TTL) + + + + pv::dialogs::Connect + + + &Scan for devices using driver above + Nach Geräten &suchen, die der ausgewählte Treiber ansprechen kann + + + + Connect to Device + Mit Gerät verbinden + + + + Step 1: Choose the driver + Schritt 1: Treiber auswählen + + + + &USB + + + + + Serial &Port + Serielle Sch&nittstelle + + + + &TCP/IP + + + + + Protocol: + Protokoll: + + + + Step 2: Choose the interface + Schritt 2: Schnittstelle auswählen + + + + Step 3: Scan for devices + Schritt 3: Nach Geräten suchen + + + + Step 4: Select the device + Schritt 4: Gerät auswählen + + + + pv::dialogs::Settings + + + + General + Allgemein + + + + Views + Ansichten + + + + + Decoders + Protokolldekoder + + + + About + Programmdetails + + + + Logging + Programminterne Meldungen + + + + User interface language + Sprache der Benutzeroberfläche + + + + User interface theme + Design der Benutzeroberfläche + + + + (You may need to restart PulseView for all UI elements to update) + (Ein Neustart von PulseView kann notwendig sein, damit alle Bedienelemente das neue Design übernehmen) + + + + System Default + Standard + + + + Qt widget style + Qt-Anzeigestil + + + + (Dark themes look best with the Fusion style) + (Dunkle Designs sehen mit dem Fusion-Stil am besten aus) + + + + Save session &setup along with .sr file + Analyse&sitzungs-Konfiguration zusammen mit .sr-Dateien speichern + + + + Trace View + Signalansicht + + + + Use colored trace &background + Verwende &farbigen Kanalhintergrund + + + + Constantly perform &zoom-to-fit during acquisition + Ständig den &Zoom anpassen, während Daten aufgezeichnet werden + + + + Perform a zoom-to-&fit when acquisition stops + Den Zoom &anpassen, wenn die Datenerfassung stoppt + + + + Show time zero at the trigger + Den Triggerzeitpunkt automatisch als Nullpunkt festlegen + + + + Always keep &newest samples at the right edge during capture + Die neuesten Datenpunkte während der Aufzeichnung immer am rechten &Rand anzeigen + + + + Show data &sampling points + Daten&punkte visuell hervorheben + + + + Fill high areas of logic signals + High-Pegel von Logiksignalen hervorheben + + + + Color to fill high areas of logic signals with + Farbe für hervorgehobene High-Pegel + + + + Show analog minor grid in addition to div grid + Vertikale Unterteilungen nochmals unterteilen + + + + Highlight mouse cursor using a vertical marker line + Position des Mauscursors durch vertikalen Balken hervorheben + + + + + + pixels + Pixel + + + + Maximum distance from edges before markers snap to them + Abstand zu Signalflanken, bevor Markierer einrasten + + + + Color to fill cursor area with + Farbe für die Auswahl-Markierung + + + + None + Keine + + + + Background + Hintergrundfarbe + + + + Dots + Farbige Abtastpunkte + + + + Conversion threshold display mode (analog traces only) + Darstellung von Konvertierungsschwellen (nur für analoge Kanäle) + + + + Default analog trace div height + Standardgröße von analogen Kanälen + + + + Default logic trace height + Standardgröße von Logikkanälen + + + + Allow configuration of &initial signal state + &Initialzustände von Signalen konfigurierbar machen + + + + Always show all &rows, even if no annotation is visible + Immer alle &Reihen anzeigen, auch wenn hierfür keine dekodierten Werte vorliegen + + + + Annotation export format + Format für zu exportierende Dekodierwerte + + + + %s = sample range; %d: decoder name; %r: row name; %c: class name + %s = Start-/Endsample; %d: Dekodername; %r: Name der Reihe; %c: Klassenname + + + + %1: longest annotation text; %a: all annotation texts; %q: use quotation marks + %1: Längste Beschreibung des dekodierten Wertes; %a: Alle Beschreibungen des dekodierten Wertes; %q: Benutze Anführungszeichen + + + %s = sample range; %d: decoder name; %r: row name; %q: use quotation marks + %s = Start-/Endsample; %d: Dekodername; %c Name der Kategorie; %q: Benutze Anführungszeichen + + + %s = sample range; %d: decoder name; %c: row name; %q: use quotations marks + %s = Start-/Endsample; %d: Dekodername; %c Name der Kategorie; %q: Benutze Anführungszeichen + + + %1: longest annotation text; %a: all annotation texts + %1: Längste Beschreibung des dekodierten Wertes; %a: Alle Beschreibungen des dekodierten Wertes + + + + %1<br /><a href="http://%2">%2</a> + + + + + GNU GPL, version 3 or later + GNU GPL, Version 3 oder neuer + + + + Versions, libraries and features: + Versionen, Bibliotheken und Features: + + + + Firmware search paths: + Suchpfade für Firmware: + + + + Protocol decoder search paths: + Suchpfade für Protokolldekoder: + + + + Supported hardware drivers: + Unterstützte Hardwaretreiber: + + + + Supported input formats: + Unterstützte Importformate: + + + + Supported output formats: + Unterstützte Exportformate: + + + + Supported protocol decoders: + Unterstützte Protokolldekoder: + + + + Log level: + Log-Level: + + + + lines + Zeilen + + + + Length of background buffer: + Länge des Logpuffers: + + + + &Save to File + &Speichern + + + + &Pop out + &Abdocken + + + + 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. + 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. + + + + 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. + 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. + + + + Save Log + Log speichern + + + + Log Files (*.txt *.log);;All Files (*) + Logdateien (*.txt *.log);;Alle Dateien (*) + + + + Success + Erfolg + + + + Log saved to %1. + Log als %1 gespeichert. + + + + Error + Fehler + + + + File %1 could not be written to. + Konnte Datei %1 nicht speichern. + + + + %1 Log + + + + + pv::dialogs::StoreProgress + + + Saving... + Speichere... + + + + Cancel + Abbrechen + + + + Failed to save session. + Beim Speichern trat ein Fehler auf. + + + + pv::popups::Channels + + + + + + All + Alle + + + + + Logic + Logik + + + + + Analog + Analog + + + + Named + Benamte + + + + Unnamed + Unbenamte + + + + Changing + Sich ändernde + + + + Non-changing + Konstante + + + + Disable: + Deaktivieren: + + + + Enable: + Aktivieren: + + + + + None + Keine + + + + pv::prop::Bool + + + + Querying config key %1 resulted in %2 + Internal message + + + + + pv::prop::Double + + + + Querying config key %1 resulted in %2 + Internal message + + + + + pv::prop::Enum + + + + Querying config key %1 resulted in %2 + Internal message + + + + + pv::prop::Int + + + + Querying config key %1 resulted in %2 + Internal message + + + + + pv::prop::String + + + + Querying config key %1 resulted in %2 + Internal message + + + + + pv::subwindows::decoder_selector::DecoderCollectionModel + + + Decoder + Dekoder + + + + Name + + + + + ID + + + + + All Decoders + Alle Dekoder + + + + pv::subwindows::decoder_selector::SubWindow + + + Select a decoder to see its description here. + Wähle einen Dekoder, um dessen Beschreibung hier lesen zu können. + + + + , %1 + + + + + <p align='right'>Tags: %1</p> + <p align='right'>Stichworte: %1</p> + + + + Protocol decoder <b>%1</b> requires input type <b>%2</b> which several decoders provide.<br>Choose which one to use:<br> + Protokolldekoder <b>%1</b> benötigt Daten vom Typ <b>%2</b>, die von verschiedenen Protokolldekodern bereitgestellt werden. <br>Wähle, welcher benutzt werden soll:<br> + + + + Choose Decoder + Wähle Protokolldekoder + + + + pv::toolbars::MainBar + + + New &View + Neue &Ansicht + + + + &Open... + &Öffnen... + + + + Restore Session Setu&p... + &Konfiguration der Analysesitzung laden... + + + + &Save As... + &Speichern als... + + + + Save Selected &Range As... + Ausgewählten &Bereich speichern als... + + + + Save Session Setu&p... + &Konfiguration der Analysesitzung speichern... + + + + &Export + + + + + &Import + + + + + &Connect to Device... + Mit Gerät &verbinden... + + + + Add protocol decoder + Protokolldekoder hinzufügen + + + + Configure Device + Gerät konfigurieren + + + + Configure Channels + Kanäle konfigurieren + + + + Failed to get sample rate list: + Konnte Liste unterstützter Abtastraten nicht abfragen: + + + + Failed to get sample rate: + Konnte Abtastrate nicht abfragen: + + + + Failed to get sample limit list: + Konnte Liste der maximal erlaubten Abtastraten nicht abfragen: + + + + Failed to configure samplerate: + Konnte Abtastrate nicht einstellen: + + + + Failed to configure sample count: + Konnte Anzahl der Abtastpunkte nicht einstellen: + + + + Missing Cursors + Fehlende Auswahl + + + + 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). + Du musst die Auswahl-Markierer setzen, bevor du die darin befindlichen Daten abspeichern kannst. Verwende hierzu bspw. den Knopf für die Auswahl-Markierer. + + + + Invalid Range + Auswahl ungültig + + + + The cursors don't define a valid range of samples. + Die Auswahl-Markierer geben keinen gültigen Datenbereich an. + + + + %1 files + %1-Dateien + + + + + All Files + Alle Dateien + + + + + Save File + Speichern + + + + Export %1 + %1 exportieren + + + + %1 files + %1-Dateien + + + + Import File + Dateiimport + + + + Import %1 + %1 importieren + + + + + Open File + Öffnen + + + + sigrok Sessions (*.sr);;All Files (*) + sigrok-Datenformat (*.sr);;Alle Dateien (*) + + + + + PulseView Session Setups (*.pvs);;All Files (*) + Analysesitzungs-Konfigurationen (*.pvs);;Alle Dateien (*) + + + + Total sampling time: %1 + Internal message + + + + + pv::views::decoder_binary::View + + + Decoder: + Dekoder: + + + + Show data as + Zeige Daten als + + + + Hexdump + Hex-Dump + + + + &Save... + &Speichern... + + + + + Save Binary Data + Binäre Daten speichern + + + + Binary Data Files (*.bin);;All Files (*) + Binärdateien (*.bin);;Alle Dateien (*) + + + + + Error + Fehler + + + + + File %1 could not be written to. + Konnte Datei %1 nicht speichern. + + + + Hex Dumps (*.txt);;All Files (*) + Hex-Dumps (*.txt);;Alle Dateien (*) + + + + pv::views::decoder_output::View + + Decoder: + Dekoder: + + + Show data as + Zeige Daten als + + + Hexdump + Hex-Dump + + + &Save... + &Speichern... + + + Save Binary Data + Binäre Daten speichern + + + Binary Data Files (*.bin);;All Files (*) + Binärdateien (*.bin);;Alle Dateien (*) + + + Error + Fehler + + + File %1 could not be written to. + Konnte Datei %1 nicht speichern. + + + Hex Dumps (*.txt);;All Files (*) + Hex-Dumps (*.txt);;Alle Dateien (*) + + + + pv::views::trace::AnalogSignal + + + Number of pos vertical divs + Anzahl Unterteilungen im Positiven + + + + Number of neg vertical divs + Anzahl Unterteilungen im Negativen + + + + pixels + Pixel + + + + Div height + Höhe einer Unterteilung + + + + V/div + V/div + + + + Vertical resolution + Vertikale Auflösung + + + + Autoranging + Automatische Skalierung + + + + none + keine + + + + to logic via threshold + zu Logik mittels Schwellwert + + + + to logic via schmitt-trigger + zu Logik mittels Schmitt-Trigger + + + + Conversion + Konvertierung + + + + Conversion threshold(s) + Konvertierungs-Schwellwert(e) + + + + analog + nur analog + + + + converted + nur konvertiert + + + + analog+converted + analog+konvertiert + + + + Show traces for + Anzuzeigende Signale + + + + pv::views::trace::Cursor + + + Disable snapping + Einrasten deaktivieren + + + + pv::views::trace::CursorPair + + + Display interval + Intervall anzeigen + + + + Display frequency + Frequenz anzeigen + + + + Display samples + Samples anzeigen + + + + pv::views::trace::DecodeTrace + + + <p><i>No decoders in the stack</i></p> + <p><i>Keine Protokolldekoder vorhanden</i></p> + + + + <i>* Required channels</i> + <i>* Notwendige Kanäle</i> + + + + Stack Decoder + Protokolldekoder stapeln + + + + Stack a higher-level decoder on top of this one + Weiteren Protokolldekoder auf diesen stapeln + + + + Delete + Löschen + + + + Resume decoding + Dekodierung fortsetzen + + + + Pause decoding + Dekodierung anhalten + + + + Copy annotation text to clipboard + Dekodierten Wert in die Zwischenablage kopieren + + + + Export all annotations + Alle dekodierten Werte exportieren + + + + Export all annotations for this row + Alle dekodierten Werte dieser Zeile exportieren + + + + Export all annotations, starting here + Alle dekodierten Werte ab hier exportieren + + + + Export annotations for this row, starting here + Alle dekodierten Werte dieser Zeile ab hier exportieren + + + + Export all annotations within cursor range + Alle dekodierten Werte innerhalb des gewählten Bereiches exportieren + + + + Export annotations for this row within cursor range + Alle dekodierten Werte dieser Zeile innerhalb des gewählten Bereiches exportieren + + + + %1: +%2 + + + + + <b>%1</b> (%2) %3 + + + + + Export annotations + Dekodierte Werte exportieren + + + + Text Files (*.txt);;All Files (*) + Textdateien (*.txt);;Alle Dateien (*) + + + + Error + Fehler + + + + File %1 could not be written to. + Konnte Datei %1 nicht speichern. + + + + Show this row + Diese Zeile anzeigen + + + + Show All + Alle anzeigen + + + + Hide All + Alle verstecken + + + + pv::views::trace::Flag + + + Text + + + + + Delete + Löschen + + + + Disable snapping + Einrasten deaktivieren + + + + pv::views::trace::Header + + + Group + Gruppieren + + + + pv::views::trace::LogicSignal + + + No trigger + Kein Trigger + + + + Trigger on rising edge + Trigger auf steigende Flanke + + + + Trigger on high level + Trigger auf High-Pegel + + + + Trigger on falling edge + Trigger auf fallende Flanke + + + + Trigger on low level + Trigger auf Low-Pegel + + + + Trigger on rising or falling edge + Trigger auf steigende oder fallende Flanke + + + + pixels + Pixel + + + + Trace height + Kanalgröße + + + + Trigger + Trigger + + + + pv::views::trace::Ruler + + + Create marker here + Hier neue Markierung anlegen + + + + Set as zero point + Als Nullpunkt setzen + + + + Reset zero point + Nullpunkt zurücksetzen + + + + Disable mouse hover marker + Mauszeigerbalken deaktivieren + + + + Enable mouse hover marker + Mauszeigerbalken aktivieren + + + + pv::views::trace::Signal + + + Name + + + + + Disable + Deaktivieren + + + + pv::views::trace::StandardBar + + + Zoom &In + H&ineinzoomen + + + + Zoom &Out + Hera&uszoomen + + + + Zoom to &Fit + &Passend zoomen + + + + Show &Cursors + &Auswahl-Markierer anzeigen + + + + Display last segment only + Nur letztes Segment anzeigen + + + + Display last complete segment only + Nur letztes vollständiges Segment anzeigen + + + + Display a single segment + Einzelnes Segment anzeigen + + + + pv::views::trace::TimeMarker + + + Time + Zeit + + + + pv::views::trace::Trace + + + Create marker here + Hier neue Markierung anlegen + + + + Color + Farbe + + + + Name + + + + + pv::views::trace::TraceGroup + + + Ungroup + Trennen + + + + pv::widgets::DecoderGroupBox + + + Show/hide this decoder trace + Dekoder anzeigen/verbergen + + + + Delete this decoder trace + Protokolldekoder entfernen + + + + pv::widgets::DeviceToolButton + + + + <No Device> + <Kein Gerät> + + + + pv::widgets::ExportMenu + + + Export %1... + %1 exportieren... + + + + pv::widgets::ImportMenu + + + Import %1... + %1 importieren... + + + diff --git a/main.cpp b/main.cpp index a77969ea..da5fffcd 100644 --- a/main.cpp +++ b/main.cpp @@ -26,6 +26,11 @@ #include #include +#ifdef ENABLE_FLOW +#include +#include +#endif + #include #include @@ -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 context; - string open_file_format, driver; + string open_file_format, open_setup_file, driver; vector 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()) { diff --git a/manual/acquisition.txt b/manual/acquisition.txt index 685c9cd8..05c50f36 100644 --- a/manual/acquisition.txt +++ b/manual/acquisition.txt @@ -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[] diff --git a/manual/analysis.txt b/manual/analysis.txt index 7aa487b4..b6547fe4 100644 --- a/manual/analysis.txt +++ b/manual/analysis.txt @@ -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. diff --git a/manual/cli.txt b/manual/cli.txt index 001b6e9f..6187dbb9 100644 --- a/manual/cli.txt +++ b/manual/cli.txt @@ -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 diff --git a/manual/decoders.txt b/manual/decoders.txt index d2ca15ab..6915f82c 100644 --- a/manual/decoders.txt +++ b/manual/decoders.txt @@ -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 diff --git a/manual/installation.txt b/manual/installation.txt index 047333ed..f890a565 100644 --- a/manual/installation.txt +++ b/manual/installation.txt @@ -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 -- diff --git a/manual/overview.txt b/manual/overview.txt index bfd87f85..bde75750 100644 --- a/manual/overview.txt +++ b/manual/overview.txt @@ -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 diff --git a/pulseview.qrc b/pulseview.qrc index 33e36100..4427a759 100644 --- a/pulseview.qrc +++ b/pulseview.qrc @@ -9,6 +9,7 @@ icons/document-new.png icons/document-open.png icons/document-save-as.png + icons/edit-paste.svg icons/help-browser.png icons/information.svg icons/media-playback-pause.png @@ -19,6 +20,7 @@ icons/settings-views.svg icons/pulseview.png icons/pulseview.svg + icons/search.svg icons/status-green.svg icons/status-grey.svg icons/status-red.svg diff --git a/pv/application.cpp b/pv/application.cpp index 5a6e28a1..6f666c5c 100644 --- a/pv/application.cpp +++ b/pv/application.cpp @@ -17,13 +17,14 @@ * along with this program; if not, see . */ -#include "application.hpp" -#include "config.h" - #include #include #include +#include +#include +#include +#include #include @@ -35,6 +36,10 @@ #include #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 context) { // Library versions and features diff --git a/pv/application.hpp b/pv/application.hpp index c618f80d..61fe46e2 100644 --- a/pv/application.hpp +++ b/pv/application.hpp @@ -23,20 +23,28 @@ #include #include +#include +#include #include +#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 context); void print_version_info(); @@ -58,6 +66,8 @@ private: vector< pair > input_format_list_; vector< pair > output_format_list_; vector< pair > pd_list_; + + QTranslator app_translator_, qt_translator_, qtbase_translator_; }; #endif // PULSEVIEW_PV_APPLICATION_HPP diff --git a/pv/binding/binding.cpp b/pv/binding/binding.cpp index 9735e146..1f17aefd 100644 --- a/pv/binding/binding.cpp +++ b/pv/binding/binding.cpp @@ -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; } diff --git a/pv/binding/decoder.cpp b/pv/binding/decoder.cpp index f51f9077..80725d2a 100644 --- a/pv/binding/decoder.cpp +++ b/pv/binding/decoder.cpp @@ -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; diff --git a/pv/data/decode/annotation.cpp b/pv/data/decode/annotation.cpp index e983b0df..8ac8d5f9 100644 --- a/pv/data/decode/annotation.cpp +++ b/pv/data/decode/annotation.cpp @@ -24,7 +24,8 @@ extern "C" { #include #include -#include "annotation.hpp" +#include +#include 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(); 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& Annotation::annotations() const +const vector* Annotation::annotations() const { return annotations_; } diff --git a/pv/data/decode/annotation.hpp b/pv/data/decode/annotation.hpp index 8b91c4f4..cfa5e5e9 100644 --- a/pv/data/decode/annotation.hpp +++ b/pv/data/decode/annotation.hpp @@ -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& annotations() const; + + Class ann_class_id() const; + const QString ann_class_name() const; + + const vector* 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 annotations_; + vector* annotations_; const Row *row_; + Class ann_class_id_; }; } // namespace decode diff --git a/pv/data/decode/decoder.cpp b/pv/data/decode/decoder.cpp index f86c5d08..e6fe82d2 100644 --- a/pv/data/decode/decoder.cpp +++ b/pv/data/decode/decoder.cpp @@ -29,7 +29,6 @@ #include #include -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& 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 Decoder::get_rows() +{ + vector 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 Decoder::ann_classes() const +{ + vector result; + + for (const AnnotationClass& c : ann_classes_) + result.push_back(&c); + + return result; +} + +vector Decoder::ann_classes() +{ + vector 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 diff --git a/pv/data/decode/decoder.hpp b/pv/data/decode/decoder.hpp index 55742c60..eb9a44bf 100644 --- a/pv/data/decode/decoder.hpp +++ b/pv/data/decode/decoder.hpp @@ -27,6 +27,9 @@ #include +#include +#include + 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_; + 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& channels() const; - void set_channels(vector channels); + bool visible() const; + void set_visible(bool visible); + + const vector& channels() const; + void set_channels(vector channels); const map& options() const; @@ -72,12 +107,26 @@ public: srd_decoder_inst* create_decoder_inst(srd_session *session); void invalidate_decoder_inst(); + vector get_rows(); + Row* get_row_by_id(size_t id); + + vector ann_classes() const; + vector 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 channels_; + vector channels_; + vector rows_; + vector ann_classes_; + vector bin_classes_; map options_; srd_decoder_inst *decoder_inst_; }; diff --git a/pv/data/decode/row.cpp b/pv/data/decode/row.cpp index 8195f3e2..f1895ae4 100644 --- a/pv/data/decode/row.cpp +++ b/pv/data/decode/row.cpp @@ -17,6 +17,9 @@ * along with this program; if not, see . */ +#include + +#include "decoder.hpp" #include "row.hpp" #include @@ -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 Row::ann_classes() const +{ + assert(decoder_); + + vector 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 diff --git a/pv/data/decode/row.hpp b/pv/data/decode/row.hpp index 34bb2373..b877b58b 100644 --- a/pv/data/decode/row.hpp +++ b/pv/data/decode/row.hpp @@ -22,7 +22,8 @@ #include -#include "annotation.hpp" +#include +#include 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 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 diff --git a/pv/data/decode/rowdata.cpp b/pv/data/decode/rowdata.cpp index 2a26169e..7b6ec2d3 100644 --- a/pv/data/decode/rowdata.cpp +++ b/pv/data/decode/rowdata.cpp @@ -17,7 +17,11 @@ * along with this program; if not, see . */ -#include "rowdata.hpp" +#include + +#include +#include +#include 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 &dest, + deque &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 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 diff --git a/pv/data/decode/rowdata.hpp b/pv/data/decode/rowdata.hpp index 0589ec89..01ea94f4 100644 --- a/pv/data/decode/rowdata.hpp +++ b/pv/data/decode/rowdata.hpp @@ -24,8 +24,9 @@ #include -#include "annotation.hpp" +#include +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 &dest, + void get_annotation_subset(deque &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 annotations_; + deque annotations_; + Row* row_; + uint64_t prev_ann_start_sample_; }; } // namespace decode diff --git a/pv/data/decodesignal.cpp b/pv/data/decodesignal.cpp index 68b17a65..f8e71999 100644 --- a/pv/data/decodesignal.cpp +++ b/pv/data/decodesignal.cpp @@ -17,6 +17,7 @@ * along with this program; if not, see . */ +#include #include #include @@ -27,23 +28,19 @@ #include "decodesignal.hpp" #include "signaldata.hpp" -#include #include #include #include #include -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 >& 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 dec = make_shared(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 dec = make_shared(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& dec : stack_) + for (const shared_ptr& 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& 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 DecodeSignal::get_channels() const +const vector DecodeSignal::get_channels() const { return channels_; } @@ -295,7 +279,7 @@ void DecodeSignal::auto_assign_signals(const shared_ptr 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 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::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 DecodeSignal::visible_rows() const +vector DecodeSignal::get_rows(bool visible_only) { - lock_guard lock(output_mutex_); - - vector rows; + vector rows; - for (const shared_ptr& dec : stack_) { + for (const shared_ptr& 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 DecodeSignal::get_rows(bool visible_only) const +{ + vector rows; + + for (const shared_ptr& 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 &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 &dest, + const Row* row, uint32_t segment_id, uint64_t start_sample, uint64_t end_sample) const { lock_guard 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 &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 *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 &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 **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 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 *all_ann_list = new forward_list(); +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 *dest) const +{ + assert(dest != nullptr); - for (const Row& row : rows) { - vector *ann_vector = new vector(); - get_annotation_subset(*ann_vector, row, segment_id, start_sample, end_sample); + try { + const DecodeSegment *segment = &(segments_.at(segment_id)); - forward_list *ann_list = - new forward_list(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 *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& decoder : stack_) { + for (const shared_ptr& 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& 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 decoder = - make_shared(dec); + shared_ptr decoder = make_shared(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 > signalbases = + const vector< shared_ptr > 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::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_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& d : stack_) + if (d->get_srd_decoder() == srd_dec) + return d.get(); + + return nullptr; +} + void DecodeSignal::update_channel_list() { - vector prev_channels = channels_; + vector prev_channels = channels_; channels_.clear(); uint16_t id = 0; // Copy existing entries, create new as needed for (shared_ptr& 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 dec : stack_) { - vector channel_list; + for (shared_ptr dec : stack_) { + vector 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 signal_in_bytepos; vector signal_in_bitpos; - for (data::DecodeChannel& ch : channels_) + for (decode::DecodeChannel& ch : channels_) if (ch.assigned_signal) { const shared_ptr 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& dec : stack_) + for (const shared_ptr& 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& dec : stack_) { + for (const shared_ptr& 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& dec : stack_) + for (const shared_ptr& 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& dec : stack_) + for (const shared_ptr& 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& 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& 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& 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()}); } } @@ -1252,37 +1409,83 @@ void DecodeSignal::annotation_callback(srd_proto_data *pdata, void *decode_signa lock_guard 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) diff --git a/pv/data/decodesignal.hpp b/pv/data/decodesignal.hpp index ba9c9b5e..cee4ccf0 100644 --- a/pv/data/decodesignal.hpp +++ b/pv/data/decodesignal.hpp @@ -21,6 +21,7 @@ #define PULSEVIEW_PV_DATA_DECODESIGNAL_HPP #include +#include #include #include #include @@ -30,6 +31,7 @@ #include +#include #include #include #include @@ -37,46 +39,49 @@ 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 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 decoder_; - const srd_channel *pdch_; + const Decoder* decoder; + const DecodeBinaryClassInfo* info; + deque chunks; }; struct DecodeSegment { - map annotation_rows; + map annotation_rows; pv::util::Timestamp start_time; double samplerate; int64_t samples_decoded_incl, samples_decoded_excl; + vector binary_classes; }; class DecodeSignal : public SignalBase @@ -93,9 +98,9 @@ public: virtual ~DecodeSignal(); bool is_decode_signal() const; - const vector< shared_ptr >& decoder_stack() const; + const vector< shared_ptr >& 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 get_channels() const; - void auto_assign_signals(const shared_ptr dec); + const vector get_channels() const; + void auto_assign_signals(const shared_ptr 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 visible_rows() const; + vector get_rows(bool visible_only=false); + vector 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 &dest, - const decode::Row &row, uint32_t segment_id, uint64_t start_sample, - uint64_t end_sample) const; + void get_annotation_subset(deque &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 &dest, - uint32_t segment_id, uint64_t start_sample, uint64_t end_sample) const; + void get_annotation_subset(deque &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 **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 *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 *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 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 channels_; + vector 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 > stack_; + vector< shared_ptr > stack_; bool stack_config_changed_; - map, decode::Row> class_rows_; vector segments_; uint32_t current_segment_id_; diff --git a/pv/data/logicsegment.cpp b/pv/data/logicsegment.cpp index 4170f642..b9e57caa 100644 --- a/pv/data/logicsegment.cpp +++ b/pv/data/logicsegment.cpp @@ -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_); diff --git a/pv/data/signalbase.cpp b/pv/data/signalbase.cpp index 78633de9..fde99e2b 100644 --- a/pv/data/signalbase.cpp +++ b/pv/data/signalbase.cpp @@ -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(); diff --git a/pv/devicemanager.cpp b/pv/devicemanager.cpp index 5090b480..09d9014b 100644 --- a/pv/devicemanager.cpp +++ b/pv/devicemanager.cpp @@ -93,7 +93,7 @@ DeviceManager::DeviceManager(shared_ptr 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) diff --git a/pv/dialogs/connect.cpp b/pv/dialogs/connect.cpp index 084b1142..d238d681 100644 --- a/pv/dialogs/connect.cpp +++ b/pv/dialogs/connect.cpp @@ -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 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::create( serial.toUtf8().constData()); + + // Set baud rate if specified + if (serial_baudrate_.currentText().length() > 0) + drvopts[ConfigKey::SERIALCOMM] = Variant::create( + QString("%1/8n1").arg(serial_baudrate_.currentText()).toUtf8().constData()); } if (tcp_config_->isEnabled()) { diff --git a/pv/dialogs/connect.hpp b/pv/dialogs/connect.hpp index 914c5878..f34268dd 100644 --- a/pv/dialogs/connect.hpp +++ b/pv/dialogs/connect.hpp @@ -89,7 +89,8 @@ private: QComboBox drivers_; - QComboBox serial_devices_; + QWidget *serial_config_; + QComboBox serial_devices_, serial_baudrate_; QWidget *tcp_config_; QLineEdit *tcp_host_; diff --git a/pv/dialogs/settings.cpp b/pv/dialogs/settings.cpp index 2bbab122..9f458c99 100644 --- a/pv/dialogs/settings.cpp +++ b/pv/dialogs/settings.cpp @@ -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(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& 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(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(); diff --git a/pv/dialogs/settings.hpp b/pv/dialogs/settings.hpp index 43988fec..50f3be65 100644 --- a/pv/dialogs/settings.hpp +++ b/pv/dialogs/settings.hpp @@ -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); diff --git a/pv/dialogs/storeprogress.cpp b/pv/dialogs/storeprogress.cpp index 9f4279ce..2bca3476 100644 --- a/pv/dialogs/storeprogress.cpp +++ b/pv/dialogs/storeprogress.cpp @@ -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(); diff --git a/pv/globalsettings.cpp b/pv/globalsettings.cpp index 34dbc832..ca649bef 100644 --- a/pv/globalsettings.cpp +++ b/pv/globalsettings.cpp @@ -17,7 +17,9 @@ * along with this program; if not, see . */ -#include "globalsettings.hpp" +#include +#include +#include #include #include @@ -29,9 +31,13 @@ #include #include +#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 > 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 GlobalSettings::callbacks_; bool GlobalSettings::tracking_ = false; +bool GlobalSettings::is_dark_theme_ = false; map 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(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 diff --git a/pv/globalsettings.hpp b/pv/globalsettings.hpp index e890a800..cf1921e8 100644 --- a/pv/globalsettings.hpp +++ b/pv/globalsettings.hpp @@ -30,6 +30,8 @@ #include #include +#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 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 diff --git a/pv/logging.cpp b/pv/logging.cpp index b747ab54..e42a9e54 100644 --- a/pv/logging.cpp +++ b/pv/logging.cpp @@ -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; diff --git a/pv/mainwindow.cpp b/pv/mainwindow.cpp index fd889e9e..ea9f01af 100644 --- a/pv/mainwindow.cpp +++ b/pv/mainwindow.cpp @@ -40,6 +40,7 @@ #include "mainwindow.hpp" +#include "application.hpp" #include "devicemanager.hpp" #include "devices/hardwaredevice.hpp" #include "dialogs/settings.hpp" @@ -49,6 +50,11 @@ #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 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 MainWindow::get_active_view() const return nullptr; } -shared_ptr MainWindow::add_view(const QString &title, - views::ViewType type, Session &session) +shared_ptr MainWindow::add_view(views::ViewType type, + Session &session) { GlobalSettings settings; shared_ptr v; @@ -142,6 +143,13 @@ shared_ptr MainWindow::add_view(const QString &title, shared_ptr 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 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(session, - (main_bar ? false : true), dock_main); + v = make_shared(session, (main_bar ? false : true), dock_main); +#ifdef ENABLE_DECODE + if (type == views::ViewTypeDecoderBinary) + v = make_shared(session, false, dock_main); +#endif if (!v) return nullptr; @@ -183,18 +194,16 @@ shared_ptr MainWindow::add_view(const QString &title, views::trace::View *tv = qobject_cast(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(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 MainWindow::add_view(const QString &title, } } + v->setFocus(); + return v; } @@ -244,6 +255,74 @@ void MainWindow::remove_view(shared_ptr view) } } +shared_ptr MainWindow::add_subwindow( + subwindows::SubWindowType type, Session &session) +{ + GlobalSettings settings; + shared_ptr 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(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 // 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 MainWindow::add_session() { static int last_session_id = 1; @@ -251,13 +330,14 @@ shared_ptr MainWindow::add_session() shared_ptr session = make_shared(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 MainWindow::add_session() window->setDockNestingEnabled(true); - shared_ptr 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) } 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 = 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& 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) 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(obj); + Session *session = qobject_cast(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(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& 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 decoder_selector = + dynamic_pointer_cast(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& 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(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 viewbase = entry.second; - - // Only trace views have this setting - views::trace::View* view = - qobject_cast(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 viewbase = entry.second; - - // Only trace views have this setting - views::trace::View* view = - qobject_cast(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 viewbase = entry.second; - - // Only trace views have this setting - views::trace::View* view = - qobject_cast(viewbase.get()); - if (view) - view->enable_show_analog_minor_grid(state); - } -} - void MainWindow::on_close_current_tab() { int tab = session_selector_.currentIndex(); diff --git a/pv/mainwindow.hpp b/pv/mainwindow.hpp index 6d92b270..522ab1c0 100644 --- a/pv/mainwindow.hpp +++ b/pv/mainwindow.hpp @@ -26,12 +26,11 @@ #include #include -#include #include #include -#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 get_active_view() const; - shared_ptr add_view(const QString &title, - views::ViewType type, Session &session); + shared_ptr add_view(views::ViewType type, Session &session); void remove_view(shared_ptr view); + shared_ptr add_subwindow( + subwindows::SubWindowType type, Session &session); + shared_ptr add_session(); void remove_session(shared_ptr 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); @@ -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 last_focused_session_; map< QDockWidget*, shared_ptr > view_docks_; + map< QDockWidget*, shared_ptr > sub_windows_; map< shared_ptr, 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_; diff --git a/pv/popups/channels.cpp b/pv/popups/channels.cpp index acd7079b..842a19d2 100644 --- a/pv/popups/channels.cpp +++ b/pv/popups/channels.cpp @@ -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 group, if (group) binding = make_shared(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("

%1

").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 > sigs) -{ int row = 0, col = 0; QGridLayout *const grid = new QGridLayout(); + vector group_checkboxes, this_row; for (const shared_ptr& 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 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) diff --git a/pv/popups/channels.hpp b/pv/popups/channels.hpp index 54d24340..972549a8 100644 --- a/pv/popups/channels.hpp +++ b/pv/popups/channels.hpp @@ -79,9 +79,6 @@ private: void populate_group(shared_ptr group, const vector< shared_ptr > sigs); - QGridLayout* create_channel_group_grid( - const vector< shared_ptr > sigs); - void showEvent(QShowEvent *event); private Q_SLOTS: diff --git a/pv/session.cpp b/pv/session.cpp index c0bcd670..6b879cf9 100644 --- a/pv/session.cpp +++ b/pv/session.cpp @@ -17,9 +17,6 @@ * along with this program; if not, see . */ -#include -#include - #include #include #include @@ -27,9 +24,13 @@ #include +#include +#include + #include "devicemanager.hpp" #include "mainwindow.hpp" #include "session.hpp" +#include "util.hpp" #include "data/analog.hpp" #include "data/analogsegment.hpp" @@ -52,6 +53,11 @@ #include +#ifdef ENABLE_FLOW +#include +#include +#endif + #ifdef ENABLE_DECODE #include #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 Session::sr_context; @@ -148,7 +167,7 @@ void Session::set_name(QString name) name_changed(); } -const list< shared_ptr > Session::views() const +const vector< shared_ptr > 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& 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& 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& vb : views_) { + shared_ptr tv = dynamic_pointer_cast(vb); + if (tv) { + for (const shared_ptr& time_item : tv->time_items()) { + + const shared_ptr flag = + dynamic_pointer_cast(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 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 dev_info; list key_list; - int decode_signals = 0, views = 0; if (device_) { shared_ptr hw_device = @@ -227,39 +324,77 @@ void Session::save_settings(QSettings &settings) const settings.endGroup(); } - // Save channels and decoders - for (const shared_ptr& base : signalbases_) { + save_setup(settings); + } +} + +void Session::restore_setup(QSettings &settings) +{ + // Restore channels + for (shared_ptr 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 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& 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 vb; + shared_ptr tv; + if (settings.contains("assoc_view")) + vb = views_.at(settings.value("assoc_view").toInt()); + + if (vb) + tv = dynamic_pointer_cast(vb); + + const QString type = settings.value("type").toString(); + + if ((type == "time_marker") && tv) { + Timestamp ts = GlobalSettings::restore_timestamp(settings, "time"); + shared_ptr 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 device; - QString device_type = settings.value("device_type").toString(); + const QString device_type = settings.value("device_type").toString(); if (device_type == "hardware") { map 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 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 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 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 sessionfile_device = + dynamic_pointer_cast(device_); + + shared_ptr inputfile_device = + dynamic_pointer_cast(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 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 input_format; map 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 format, - const map &options) +void Session::load_file(QString file_name, QString setup_file_name, + shared_ptr format, const map &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 view) { - if (views_.empty()) { + if (views_.empty()) main_view_ = view; - } views_.push_back(view); @@ -636,35 +760,29 @@ void Session::register_view(shared_ptr view) update_signals(); // Add all other signals - unordered_set< shared_ptr > view_signalbases = - view->signalbases(); - - views::trace::View *trace_view = - qobject_cast(view.get()); - - if (trace_view) { - for (const shared_ptr& signalbase : signalbases_) { - const int sb_exists = count_if( - view_signalbases.cbegin(), view_signalbases.cend(), - [&](const shared_ptr &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 > view_signalbases = view->signalbases(); + + for (const shared_ptr& signalbase : signalbases_) { + const int sb_exists = count_if( + view_signalbases.cbegin(), view_signalbases.cend(), + [&](const shared_ptr &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(signalbase)); + view->add_decode_signal(dynamic_pointer_cast(signalbase)); #endif - break; - case data::SignalBase::MathChannel: - // TBD - break; - } - } + break; + } } signals_changed(); @@ -672,7 +790,9 @@ void Session::register_view(shared_ptr view) void Session::deregister_view(shared_ptr view) { - views_.remove_if([&](shared_ptr v) { return v == view; }); + views_.erase(std::remove_if(views_.begin(), views_.end(), + [&](shared_ptr v) { return v == view; }), + views_.end()); if (views_.empty()) { main_view_.reset(); @@ -732,7 +852,7 @@ vector Session::get_triggers(uint32_t segment_id) const return result; } -const unordered_set< shared_ptr > Session::signalbases() const +const vector< shared_ptr > Session::signalbases() const { return signalbases_; } @@ -757,7 +877,7 @@ shared_ptr Session::add_decode_signal() // Create the decode signal signal = make_shared(*this); - signalbases_.insert(signal); + signalbases_.push_back(signal); // Add the decode signal to all views for (shared_ptr& view : views_) @@ -774,7 +894,9 @@ shared_ptr Session::add_decode_signal() void Session::remove_decode_signal(shared_ptr signal) { - signalbases_.erase(signal); + signalbases_.erase(std::remove_if(signalbases_.begin(), signalbases_.end(), + [&](shared_ptr s) { return s == signal; }), + signalbases_.end()); for (shared_ptr& 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 lock(sampling_mutex_); changed = capture_state_ != state; @@ -853,18 +980,17 @@ void Session::update_signals() qobject_cast(viewbase.get()); if (trace_view) { - unordered_set< shared_ptr > - prev_sigs(trace_view->signals()); + vector< shared_ptr > prev_sigs(trace_view->signals()); trace_view->clear_signals(); for (auto channel : sr_dev->channels()) { shared_ptr signalbase; - shared_ptr signal; + shared_ptr signal; // Find the channel in the old signals const auto iter = find_if( prev_sigs.cbegin(), prev_sigs.cend(), - [&](const shared_ptr &s) { + [&](const shared_ptr &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; + switch(channel->type()->id()) { case SR_CHANNEL_LOGIC: if (!signalbase) { signalbase = make_shared(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( - new views::trace::LogicSignal(*this, - device_, signalbase)); - trace_view->add_signal(signal); + signal = shared_ptr(new LogicSignal(*this, device_, signalbase)); break; case SR_CHANNEL_ANALOG: @@ -903,7 +1028,7 @@ void Session::update_signals() if (!signalbase) { signalbase = make_shared(channel, data::SignalBase::AnalogChannel); - signalbases_.insert(signalbase); + signalbases_.push_back(signalbase); shared_ptr 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( - new views::trace::AnalogSignal( - *this, signalbase)); - trace_view->add_signal(signal); + signal = shared_ptr(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 main_tv = + dynamic_pointer_cast(main_view_); + shared_ptr 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 error_handler) { assert(error_handler); +#ifdef ENABLE_FLOW + pipeline_ = Pipeline::create(); + + source_ = ElementFactory::create_element("filesrc", "source"); + sink_ = RefPtr::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 = 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 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 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 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& bus, const Glib::RefPtr& 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 sample = sink_->pull_sample(); + RefPtr buf = sample->get_buffer(); + + for (uint32_t block_id = 0; block_id < buf->n_memory(); block_id++) { + RefPtr buf_mem = buf->get_memory(block_id); + Gst::MapInfo mapinfo; + buf_mem->map(mapinfo, Gst::MAP_READ); + + shared_ptr logic_packet = + sr_context->create_logic_packet(mapinfo.get_data(), buf->get_size(), 1); + + try { + feed_in_logic(dynamic_pointer_cast(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) 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(ConfigKey::SAMPLERATE); @@ -1348,4 +1561,19 @@ void Session::on_data_saved() data_saved_ = true; } +#ifdef ENABLE_DECODE +void Session::on_new_decoders_selected(vector decoders) +{ + assert(decoders.size() > 0); + + shared_ptr 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 diff --git a/pv/session.hpp b/pv/session.hpp index 2ee31cfe..095c4dd0 100644 --- a/pv/session.hpp +++ b/pv/session.hpp @@ -20,6 +20,11 @@ #ifndef PULSEVIEW_PV_SESSION_HPP #define PULSEVIEW_PV_SESSION_HPP +#ifdef ENABLE_FLOW +#include +#include +#endif + #include #include #include @@ -33,12 +38,18 @@ #include #include #include +#include + +#ifdef ENABLE_FLOW +#include +#include +#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() const; + const vector< shared_ptr > views() const; shared_ptr 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 format = nullptr, const map &options = map()); @@ -174,7 +199,7 @@ public: bool has_view(shared_ptr view); - const unordered_set< shared_ptr > signalbases() const; + const vector< shared_ptr > 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& bus, const Glib::RefPtr& message); + + Gst::FlowReturn on_gst_new_sample(); +#endif + void feed_in_header(); void feed_in_meta(shared_ptr 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 decoders); +#endif + private: DeviceManager &device_manager_; shared_ptr device_; QString default_name_, name_; - list< shared_ptr > views_; + vector< shared_ptr > views_; shared_ptr main_view_; shared_ptr main_bar_; @@ -253,7 +287,7 @@ private: mutable mutex sampling_mutex_; //!< Protects access to capture_state_. capture_state capture_state_; - unordered_set< shared_ptr > signalbases_; + vector< shared_ptr > signalbases_; unordered_set< shared_ptr > all_signal_data_; /// trigger_list_ contains pairs of values. @@ -272,6 +306,18 @@ private: bool out_of_memory_; bool data_saved_; bool frame_began_; + + QElapsedTimer acq_time_; + +#ifdef ENABLE_FLOW + RefPtr pipeline_; + RefPtr source_; + RefPtr sink_; + + mutable mutex pipeline_done_mutex_; + mutable condition_variable pipeline_done_cond_; + atomic pipeline_done_interrupt_; +#endif }; } // namespace pv diff --git a/pv/storesession.cpp b/pv/storesession.cpp index ee1a3a03..ed0b0def 100644 --- a/pv/storesession.cpp +++ b/pv/storesession.cpp @@ -21,6 +21,8 @@ #include "storesession.hpp" +#include + #include #include #include @@ -28,6 +30,7 @@ #include #include #include +#include #include #include @@ -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 > sigs(session_.signalbases()); + const vector< shared_ptr > sigs(session_.signalbases()); shared_ptr any_segment; shared_ptr 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 > 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 > 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 achannel = (achannel_list.at(i))->channel(); shared_ptr asegment = asegment_list.at(i); @@ -286,6 +301,11 @@ void StoreSession::store_proc(vector< shared_ptr > 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 index 00000000..0ce5ab73 --- /dev/null +++ b/pv/subwindows/decoder_selector/item.cpp @@ -0,0 +1,95 @@ +/* + * This file is part of the PulseView project. + * + * Copyright (C) 2018 Soeren Apel + * + * This 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 . + */ + +#include "subwindow.hpp" + +using std::out_of_range; + +namespace pv { +namespace subwindows { +namespace decoder_selector { + +DecoderCollectionItem::DecoderCollectionItem(const vector& data, + shared_ptr parent) : + data_(data), + parent_(parent) +{ +} + +void DecoderCollectionItem::appendSubItem(shared_ptr item) +{ + subItems_.push_back(item); +} + +shared_ptr DecoderCollectionItem::subItem(int row) const +{ + try { + return subItems_.at(row); + } catch (out_of_range&) { + return nullptr; + } +} + +shared_ptr DecoderCollectionItem::parent() const +{ + return parent_; +} + +shared_ptr DecoderCollectionItem::findSubItem( + const QVariant& value, int column) +{ + for (shared_ptr 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(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 index 00000000..ebdfd9d7 --- /dev/null +++ b/pv/subwindows/decoder_selector/model.cpp @@ -0,0 +1,206 @@ +/* + * This file is part of the PulseView project. + * + * Copyright (C) 2018 Soeren Apel + * + * This 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 . + */ + +#include + +#include "subwindow.hpp" + +#include + +#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 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(header_data); + + // Note: the tag groups are sub-items of the root item + + // Create "all decoders" group + vector 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 group_item_all = + make_shared(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 decoder_item_all = + make_shared(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 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(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 decoder_item = + make_shared(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(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(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(index.internalPointer()); + + shared_ptr 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(parent_idx.internalPointer()); + + return parent->subItemCount(); +} + +int DecoderCollectionModel::columnCount(const QModelIndex& parent_idx) const +{ + if (parent_idx.isValid()) + return static_cast( + 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 index 00000000..94ed6f4b --- /dev/null +++ b/pv/subwindows/decoder_selector/subwindow.cpp @@ -0,0 +1,369 @@ +/* + * This file is part of the PulseView project. + * + * Copyright (C) 2018 Soeren Apel + * + * This 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 . + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pv/session.hpp" +#include "pv/subwindows/decoder_selector/subwindow.hpp" + +#include +#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)), + &session, SLOT(on_new_decoders_selected(vector))); + + // 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 SubWindow::get_decoder_inputs(const srd_decoder* d) const +{ + vector ret_val; + + for (GSList* li = d->inputs; li; li = li->next) + ret_val.push_back((const char*)li->data); + + return ret_val; +} + +vector SubWindow::get_decoders_providing(const char* output) const +{ + vector 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("%1 (%2)
%3") + .arg(longname, id, desc)); + else + info_label_header_->clear(); + + info_label_body_->setText(doc); + + if (!tags.isEmpty()) + info_label_footer_->setText(tr("

Tags: %1

").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 decoders; + decoders.push_back(chosen_decoder); + + // If the decoder only depends on logic inputs, we add it and are done + vector 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 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 %1 requires input type %2 " \ + "which several decoders provide.
Choose which one to use:
")) + .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 index 00000000..c189fb24 --- /dev/null +++ b/pv/subwindows/decoder_selector/subwindow.hpp @@ -0,0 +1,155 @@ +/* + * This file is part of the PulseView project. + * + * Copyright (C) 2018 Soeren Apel + * + * This 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 . + */ + +#ifndef PULSEVIEW_PV_SUBWINDOWS_DECODERSELECTOR_SUBWINDOW_HPP +#define PULSEVIEW_PV_SUBWINDOWS_DECODERSELECTOR_SUBWINDOW_HPP + +#include + +#include +#include +#include +#include +#include + +#include "pv/subwindows/subwindowbase.hpp" + +using std::shared_ptr; + +namespace pv { +namespace subwindows { +namespace decoder_selector { + +class DecoderCollectionItem +{ +public: + DecoderCollectionItem(const vector& data, + shared_ptr parent = nullptr); + + void appendSubItem(shared_ptr item); + + shared_ptr subItem(int row) const; + shared_ptr parent() const; + shared_ptr findSubItem(const QVariant& value, int column); + + int subItemCount() const; + int columnCount() const; + int row() const; + QVariant data(int column) const; + +private: + vector< shared_ptr > subItems_; + vector data_; + shared_ptr 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 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 get_decoder_inputs(const srd_decoder* d) const; + + /** + * Returns a list of protocol decoder IDs which provide a given output + * ("uart", "spi", etc.) + */ + vector get_decoders_providing(const char* output) const; + +Q_SIGNALS: + void new_decoders_selected(vector 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 index 00000000..f81b3582 --- /dev/null +++ b/pv/subwindows/subwindowbase.cpp @@ -0,0 +1,111 @@ +/* + * This file is part of the PulseView project. + * + * Copyright (C) 2018 Soeren Apel + * + * This 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 . + */ + +#ifdef ENABLE_DECODE +#include +#endif + +#include + +#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 > SubWindowBase::signalbases() const +{ + return signalbases_; +} + +void SubWindowBase::clear_signalbases() +{ + for (const shared_ptr& 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 signalbase) +{ + signalbases_.insert(signalbase); +} + +#ifdef ENABLE_DECODE +void SubWindowBase::clear_decode_signals() +{ +} + +void SubWindowBase::add_decode_signal(shared_ptr signal) +{ + (void)signal; +} + +void SubWindowBase::remove_decode_signal(shared_ptr 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 index 00000000..ab3a08b4 --- /dev/null +++ b/pv/subwindows/subwindowbase.hpp @@ -0,0 +1,93 @@ +/* + * This file is part of the PulseView project. + * + * Copyright (C) 2018 Soeren Apel + * + * This 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 . + */ + +#ifndef PULSEVIEW_PV_SUBWINDOWBASE_HPP +#define PULSEVIEW_PV_SUBWINDOWBASE_HPP + +#include +#include +#include + +#include +#include + +#include + +#ifdef ENABLE_DECODE +#include +#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 > signalbases() const; + + virtual void clear_signalbases(); + + virtual void add_signalbase(const shared_ptr signalbase); + +#ifdef ENABLE_DECODE + virtual void clear_decode_signals(); + + virtual void add_decode_signal(shared_ptr signal); + + virtual void remove_decode_signal(shared_ptr signal); +#endif + + virtual int minimum_width() const; + +public Q_SLOTS: + virtual void on_signals_changed(); + +protected: + Session &session_; + + unordered_set< shared_ptr > signalbases_; +}; + +} // namespace subwindows +} // namespace pv + +#endif // PULSEVIEW_PV_SUBWINDOWBASE_HPP diff --git a/pv/toolbars/mainbar.cpp b/pv/toolbars/mainbar.cpp index e6beb2b3..a7998b40 100644 --- a/pv/toolbars/mainbar.cpp +++ b/pv/toolbars/mainbar.cpp @@ -35,7 +35,6 @@ #include -#include #include #include #include @@ -51,7 +50,7 @@ #include #include #ifdef ENABLE_DECODE -#include +#include #endif #include @@ -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 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)), this, SLOT(import_file(shared_ptr))); @@ -170,12 +200,16 @@ MainBar::MainBar(Session &session, QWidget *parent, pv::views::trace::View *view open_button_->setPopupMode(QToolButton::MenuButtonPopup); // Save button - vector open_actions; - open_actions.push_back(action_save_as_); - open_actions.push_back(action_save_selection_as_); + vector 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)), this, SLOT(export_file(shared_ptr))); @@ -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 signal = session_.add_decode_signal(); - if (signal) - signal->stack_decoder(decoder); -#else - (void)decoder; -#endif -} - void MainBar::export_file(shared_ptr format, bool selection_only) { using pv::dialogs::StoreProgress; @@ -701,7 +721,7 @@ void MainBar::import_file(shared_ptr 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_); diff --git a/pv/toolbars/mainbar.hpp b/pv/toolbars/mainbar.hpp index e938dbbc..04c344c5 100644 --- a/pv/toolbars/mainbar.hpp +++ b/pv/toolbars/mainbar.hpp @@ -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 format, bool selection_only = false); void import_file(shared_ptr 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 }; diff --git a/pv/util.cpp b/pv/util.cpp index 49b9467c..9a9a5065 100644 --- a/pv/util.cpp +++ b/pv/util.cpp @@ -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); diff --git a/pv/util.hpp b/pv/util.hpp index dd7be222..e1640c4a 100644 --- a/pv/util.hpp +++ b/pv/util.hpp @@ -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 index 00000000..cdaf1b5d --- /dev/null +++ b/pv/views/decoder_binary/QHexView.cpp @@ -0,0 +1,689 @@ +/* + * This file is part of the PulseView project. + * + * Copyright (C) 2015 Victor Anjin + * Copyright (C) 2019 Soeren Apel + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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 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 index 00000000..c39dcb20 --- /dev/null +++ b/pv/views/decoder_binary/QHexView.hpp @@ -0,0 +1,101 @@ +/* + * This file is part of the PulseView project. + * + * Copyright (C) 2015 Victor Anjin + * Copyright (C) 2019 Soeren Apel + * + * 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 + +#include + +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 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 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 index 00000000..5f55b191 --- /dev/null +++ b/pv/views/decoder_binary/view.cpp @@ -0,0 +1,480 @@ +/* + * This file is part of the PulseView project. + * + * Copyright (C) 2019 Soeren Apel + * + * This 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 . + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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 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& 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& 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 signal) +{ + // Remove all decoders provided by this signal + for (const shared_ptr& 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 selection = hex_view_->get_selection(); + + vector 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 selection = hex_view_->get_selection(); + + vector 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(); + + // Find the signal that contains the selected decoder + for (const shared_ptr& ds : decode_signals_) + for (const shared_ptr& 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(); + + 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(QObject::sender()); + assert(sb); + + DecodeSignal* signal = dynamic_cast(sb); + assert(signal); + + // Update all decoder entries provided by this signal + auto stack = signal->decoder_stack(); + if (stack.size() > 1) { + for (const shared_ptr& 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& 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); + + // 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& ds : decode_signals_) + for (const shared_ptr& 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); + + // 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 index 00000000..c1d70b1b --- /dev/null +++ b/pv/views/decoder_binary/view.hpp @@ -0,0 +1,115 @@ +/* + * This file is part of the PulseView project. + * + * Copyright (C) 2019 Soeren Apel + * + * This 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 . + */ + +#ifndef PULSEVIEW_PV_VIEWS_DECODERBINARY_VIEW_HPP +#define PULSEVIEW_PV_VIEWS_DECODERBINARY_VIEW_HPP + +#include +#include +#include +#include + +#include +#include + +#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 signal); + virtual void remove_decode_signal(shared_ptr 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 diff --git a/pv/views/trace/analogsignal.cpp b/pv/views/trace/analogsignal.cpp index cd680085..a0dd9eb2 100644 --- a/pv/views/trace/analogsignal.cpp +++ b/pv/views/trace/analogsignal.cpp @@ -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 base) : Signal(session, base), + value_at_hover_pos_(std::numeric_limits::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::quiet_NaN()) + autoranging_(true) { axis_pen_ = AxisPen; @@ -143,38 +143,48 @@ shared_ptr AnalogSignal::data() const return base_->analog_data(); } -void AnalogSignal::save_settings(QSettings &settings) const +std::map 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 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 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& 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::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::quiet_NaN(); - } } } diff --git a/pv/views/trace/analogsignal.hpp b/pv/views/trace/analogsignal.hpp index fd724492..c588eb88 100644 --- a/pv/views/trace/analogsignal.hpp +++ b/pv/views/trace/analogsignal.hpp @@ -77,9 +77,8 @@ public: shared_ptr data() const; - virtual void save_settings(QSettings &settings) const; - - virtual void restore_settings(QSettings &settings); + virtual std::map save_settings() const; + virtual void restore_settings(std::map 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 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 diff --git a/pv/views/trace/cursor.cpp b/pv/views/trace/cursor.cpp index 7a375c7a..80eaba23 100644 --- a/pv/views/trace/cursor.cpp +++ b/pv/views/trace/cursor.cpp @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -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::get_other_cursor() const { const shared_ptr cursors(view_.cursors()); diff --git a/pv/views/trace/cursor.hpp b/pv/views/trace/cursor.hpp index c3960d6d..0da72e9f 100644 --- a/pv/views/trace/cursor.hpp +++ b/pv/views/trace/cursor.hpp @@ -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 get_other_cursor() const; diff --git a/pv/views/trace/cursorpair.cpp b/pv/views/trace/cursorpair.cpp index 933eb8ff..7d7d8e4d 100644 --- a/pv/views/trace/cursorpair.cpp +++ b/pv/views/trace/cursorpair.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #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()); + show_frequency_ = settings.value( + GlobalSettings::Key_View_CursorShowFrequency).value(); + show_interval_ = settings.value( + GlobalSettings::Key_View_CursorShowInterval).value(); + show_samples_ = settings.value( + GlobalSettings::Key_View_CursorShowSamples).value(); 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()); + }); + + 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()); + }); + + 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()); + }); + + 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 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 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()); + + if (key == GlobalSettings::Key_View_CursorShowFrequency) + show_frequency_ = value.value(); + + if (key == GlobalSettings::Key_View_CursorShowInterval) + show_interval_ = value.value(); + + if (key == GlobalSettings::Key_View_CursorShowSamples) + show_samples_ = value.value(); } 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(), 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()); + 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 diff --git a/pv/views/trace/cursorpair.hpp b/pv/views/trace/cursorpair.hpp index 9d450df6..d59d9414 100644 --- a/pv/views/trace/cursorpair.hpp +++ b/pv/views/trace/cursorpair.hpp @@ -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 query_size + = [](const QString& s) -> double { (void)s; return 0; }); pair 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 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 diff --git a/pv/views/trace/decodetrace.cpp b/pv/views/trace/decodetrace.cpp index 9c7196bf..1cc89feb 100644 --- a/pv/views/trace/decodetrace.cpp +++ b/pv/views/trace/decodetrace.cpp @@ -31,7 +31,10 @@ extern "C" { #include #include +#include +#include #include +#include #include #include #include @@ -55,56 +58,117 @@ extern "C" { #include #include #include +#include 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 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(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 DecodeTrace::base() const return base_; } -pair 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 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 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 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 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 annotations; - decode_signal_->get_annotation_subset(annotations, row, + deque 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 > &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 sample_range = - get_view_sample_range(click_pos.x(), click_pos.x() + 1); + const pair sample_range = get_view_sample_range(pos.x(), pos.x() + 1); selected_sample_range_ = make_pair(sample_range.first, numeric_limits::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 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& 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 Annotation* prev_ann; qreal prev_end = INT_MIN; qreal a_end; @@ -451,17 +711,11 @@ void DecodeTrace::draw_annotations(vector 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 Annotation* a : 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 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 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 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 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 annotations = a.annotations(); + const qreal top = y + .5 - annotation_height_ / 2; + const qreal bottom = y + .5 + annotation_height_ / 2; + const vector* 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 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 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 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 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 &dec, QWidget *parent, - QFormLayout *form) +void DecodeTrace::create_decoder_form(int index, shared_ptr &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 *annotations) const +void DecodeTrace::export_annotations(deque& 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 *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 *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 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 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 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(); + 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(); + 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(); + 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(); + 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 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 *annotations = new vector(); + deque 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 *annotations = new vector(); + deque 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 diff --git a/pv/views/trace/decodetrace.hpp b/pv/views/trace/decodetrace.hpp index 3d25c3e4..258509e6 100644 --- a/pv/views/trace/decodetrace.hpp +++ b/pv/views/trace/decodetrace.hpp @@ -20,6 +20,7 @@ #ifndef PULSEVIEW_PV_VIEWS_TRACEVIEW_DECODETRACE_HPP #define PULSEVIEW_PV_VIEWS_TRACEVIEW_DECODETRACE_HPP +#include #include "trace.hpp" #include @@ -29,20 +30,34 @@ #include #include +#include +#include +#include +#include #include #include #include +#include #include #include #include +#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 selectors; + + QColor row_color; + map ann_class_color; + map 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 signalbase, + DecodeTrace(pv::Session &session, shared_ptr signalbase, int index); + ~DecodeTrace(); + bool enabled() const; - shared_ptr base() const; + shared_ptr 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 annotations, - QPainter &p, int h, const ViewItemPaintParams &pp, int y, - QColor row_color, int row_title_width); + void draw_annotations(deque& 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 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 &dec, + void update_stack_button(); + + void create_decoder_form(int index, shared_ptr &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 *annotations) const; + void export_annotations(deque& 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 decode_signal_; - vector visible_rows_; + deque rows_; + mutable mutex row_modification_mutex_; map channel_id_map_; // channel selector -> decode channel ID map init_state_map_; // init state selector -> decode channel ID list< shared_ptr > bindings_; - data::decode::Row *selected_row_; + const Row* selected_row_; pair selected_sample_range_; vector decoder_forms_; + QPushButton* stack_button_; - map 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 diff --git a/pv/views/trace/flag.cpp b/pv/views/trace/flag.cpp index 64ef1047..b0518b64 100644 --- a/pv/views/trace/flag.cpp +++ b/pv/views/trace/flag.cpp @@ -19,11 +19,13 @@ #include "timemarker.hpp" #include "view.hpp" +#include "ruler.hpp" #include #include #include #include +#include #include @@ -57,7 +59,58 @@ bool Flag::enabled() const QString Flag::get_text() const { - return text_; + QString s; + + const shared_ptr 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 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 diff --git a/pv/views/trace/flag.hpp b/pv/views/trace/flag.hpp index 4bf6ebd5..e58771b8 100644 --- a/pv/views/trace/flag.hpp +++ b/pv/views/trace/flag.hpp @@ -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(); diff --git a/pv/views/trace/header.cpp b/pv/views/trace/header.cpp index d7da7e03..1e97521a 100644 --- a/pv/views/trace/header.cpp +++ b/pv/views/trace/header.cpp @@ -99,16 +99,16 @@ void Header::paintEvent(QPaintEvent*) { const QRect rect(0, 0, width(), height()); - vector< shared_ptr > items(view_.list_by_type()); + vector< shared_ptr > items(view_.list_by_type()); stable_sort(items.begin(), items.end(), - [](const shared_ptr &a, const shared_ptr &b) { + [](const shared_ptr &a, const shared_ptr &b) { return a->drag_point(QRect()).y() < b->drag_point(QRect()).y(); }); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); - for (const shared_ptr& r : items) { + for (const shared_ptr& r : items) { assert(r); const bool highlight = !item_dragging_ && diff --git a/pv/views/trace/logicsignal.cpp b/pv/views/trace/logicsignal.cpp index f9ab16f1..a3fd7a0b 100644 --- a/pv/views/trace/logicsignal.cpp +++ b/pv/views/trace/logicsignal.cpp @@ -146,16 +146,21 @@ shared_ptr LogicSignal::logic_data() const return base_->logic_data(); } -void LogicSignal::save_settings(QSettings &settings) const +std::map LogicSignal::save_settings() const { - settings.setValue("trace_height", signal_height_); + std::map result; + + result["trace_height"] = signal_height_; + + return result; } -void LogicSignal::restore_settings(QSettings &settings) +void LogicSignal::restore_settings(std::map 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 diff --git a/pv/views/trace/logicsignal.hpp b/pv/views/trace/logicsignal.hpp index b170e2c0..b769ec55 100644 --- a/pv/views/trace/logicsignal.hpp +++ b/pv/views/trace/logicsignal.hpp @@ -81,8 +81,8 @@ public: shared_ptr logic_data() const; - virtual void save_settings(QSettings &settings) const; - virtual void restore_settings(QSettings &settings); + virtual std::map save_settings() const; + virtual void restore_settings(std::map settings); /** * Computes the vertical extents of the contents of this row item. diff --git a/pv/views/trace/marginwidget.cpp b/pv/views/trace/marginwidget.cpp index 86ec069b..537ffe5e 100644 --- a/pv/views/trace/marginwidget.cpp +++ b/pv/views/trace/marginwidget.cpp @@ -47,6 +47,9 @@ void MarginWidget::item_clicked(const shared_ptr &item) void MarginWidget::show_popup(const shared_ptr &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 diff --git a/pv/views/trace/marginwidget.hpp b/pv/views/trace/marginwidget.hpp index 9a0686aa..ef5e6350 100644 --- a/pv/views/trace/marginwidget.hpp +++ b/pv/views/trace/marginwidget.hpp @@ -61,10 +61,12 @@ protected: */ void show_popup(const shared_ptr &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 index c57043d4..00000000 --- a/pv/views/trace/rowitem.cpp +++ /dev/null @@ -1,33 +0,0 @@ -/* - * This file is part of the PulseView project. - * - * Copyright (C) 2015 Joel Holdsworth - * - * This 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 . - */ - -#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 index d2a205fb..00000000 --- a/pv/views/trace/rowitem.hpp +++ /dev/null @@ -1,41 +0,0 @@ -/* - * This file is part of the PulseView project. - * - * Copyright (C) 2013 Joel Holdsworth - * - * This 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 . - */ - -#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 diff --git a/pv/views/trace/ruler.cpp b/pv/views/trace/ruler.cpp index acea8a36..555794fc 100644 --- a/pv/views/trace/ruler.cpp +++ b/pv/views/trace/ruler.cpp @@ -19,7 +19,6 @@ #include -#include #include #include #include @@ -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() != 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 > Ruler::items() time_items.begin(), time_items.end()); } +void Ruler::item_hover(const shared_ptr &item, QPoint pos) +{ + (void)pos; + + hover_item_ = dynamic_pointer_cast(item); +} + +shared_ptr 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 ref_item; + const vector< shared_ptr > 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 Ruler::get_mouse_over_item(const QPoint &pt) { const vector< shared_ptr > 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() diff --git a/pv/views/trace/ruler.hpp b/pv/views/trace/ruler.hpp index b1414872..9d708ce0 100644 --- a/pv/views/trace/ruler.hpp +++ b/pv/views/trace/ruler.hpp @@ -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 get_reference_item() const; protected: virtual void contextMenuEvent(QContextMenuEvent *event) override; void resizeEvent(QResizeEvent*) override; + virtual void item_hover(const shared_ptr &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 tick_position_cache_; + shared_ptr hover_item_; + uint32_t context_menu_x_pos_; }; diff --git a/pv/views/trace/signal.cpp b/pv/views/trace/signal.cpp index 0c0cde05..b770dee3 100644 --- a/pv/views/trace/signal.cpp +++ b/pv/views/trace/signal.cpp @@ -90,14 +90,34 @@ shared_ptr Signal::base() const void Signal::save_settings(QSettings &settings) const { - (void)settings; + std::map settings_map = save_settings(); + + for (auto& entry : settings_map) + settings.setValue(entry.first, entry.second); +} + +std::map Signal::save_settings() const +{ + return std::map(); } void Signal::restore_settings(QSettings &settings) +{ + std::map 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 settings) { (void)settings; } + void Signal::paint_back(QPainter &p, ViewItemPaintParams &pp) { if (base_->enabled()) diff --git a/pv/views/trace/signal.hpp b/pv/views/trace/signal.hpp index 1b9f2543..c9f38dd9 100644 --- a/pv/views/trace/signal.hpp +++ b/pv/views/trace/signal.hpp @@ -23,6 +23,8 @@ #include #include +#include +#include #include #include @@ -87,8 +89,10 @@ public: shared_ptr base() const; virtual void save_settings(QSettings &settings) const; + virtual std::map save_settings() const; virtual void restore_settings(QSettings &settings); + virtual void restore_settings(std::map settings); void paint_back(QPainter &p, ViewItemPaintParams &pp); diff --git a/pv/views/trace/standardbar.cpp b/pv/views/trace/standardbar.cpp index 22697506..48d92891 100644 --- a/pv/views/trace/standardbar.cpp +++ b/pv/views/trace/standardbar.cpp @@ -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); } diff --git a/pv/views/trace/timeitem.cpp b/pv/views/trace/timeitem.cpp index 47a7da3c..3de62258 100644 --- a/pv/views/trace/timeitem.cpp +++ b/pv/views/trace/timeitem.cpp @@ -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 diff --git a/pv/views/trace/timeitem.hpp b/pv/views/trace/timeitem.hpp index ba858254..06d85050 100644 --- a/pv/views/trace/timeitem.hpp +++ b/pv/views/trace/timeitem.hpp @@ -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_; }; diff --git a/pv/views/trace/timemarker.cpp b/pv/views/trace/timemarker.cpp index 266007a5..b428f602 100644 --- a/pv/views/trace/timemarker.cpp +++ b/pv/views/trace/timemarker.cpp @@ -25,6 +25,7 @@ #include "timemarker.hpp" #include "pv/widgets/timestampspinbox.hpp" +#include "ruler.hpp" #include "view.hpp" #include @@ -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 diff --git a/pv/views/trace/timemarker.hpp b/pv/views/trace/timemarker.hpp index 48afc16b..cd7c84d2 100644 --- a/pv/views/trace/timemarker.hpp +++ b/pv/views/trace/timemarker.hpp @@ -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 diff --git a/pv/views/trace/trace.cpp b/pv/views/trace/trace.cpp index 5c854aed..6d44da2c 100644 --- a/pv/views/trace/trace.cpp +++ b/pv/views/trace/trace.cpp @@ -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 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 diff --git a/pv/views/trace/tracetreeitem.hpp b/pv/views/trace/tracetreeitem.hpp index 3605aa1a..51e2ba42 100644 --- a/pv/views/trace/tracetreeitem.hpp +++ b/pv/views/trace/tracetreeitem.hpp @@ -24,7 +24,7 @@ #include -#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 { 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. diff --git a/pv/views/trace/tracetreeitemowner.cpp b/pv/views/trace/tracetreeitemowner.cpp index ccf9b912..95ea8373 100644 --- a/pv/views/trace/tracetreeitemowner.cpp +++ b/pv/views/trace/tracetreeitemowner.cpp @@ -114,7 +114,7 @@ void TraceTreeItemOwner::restack_items() { vector> 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 &a, const shared_ptr &b) { const auto aext = a->v_extents(); diff --git a/pv/views/trace/triggermarker.cpp b/pv/views/trace/triggermarker.cpp index 3311f350..843ccd49 100644 --- a/pv/views/trace/triggermarker.cpp +++ b/pv/views/trace/triggermarker.cpp @@ -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(); diff --git a/pv/views/trace/triggermarker.hpp b/pv/views/trace/triggermarker.hpp index a97fefb0..222d3fb9 100644 --- a/pv/views/trace/triggermarker.hpp +++ b/pv/views/trace/triggermarker.hpp @@ -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; /** diff --git a/pv/views/trace/view.cpp b/pv/views/trace/view.cpp index 79277b4e..227fadfd 100644 --- a/pv/views/trace/view.cpp +++ b/pv/views/trace/view.cpp @@ -31,11 +31,8 @@ #include #include -#include -#include -#include - #include +#include #include #include #include @@ -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 > View::signals() const +vector< shared_ptr > View::signals() const { return signals_; } +shared_ptr View::get_signal_by_signalbase(shared_ptr base) const +{ + shared_ptr ret_val; + + for (const shared_ptr& 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) { 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) #ifdef ENABLE_DECODE void View::clear_decode_signals() { + ViewBase::clear_decode_signals(); decode_traces_.clear(); } void View::add_decode_signal(shared_ptr signal) { + ViewBase::add_decode_signal(signal); + shared_ptr d( new DecodeTrace(session_, signal, decode_traces_.size())); decode_traces_.push_back(d); @@ -320,6 +384,8 @@ void View::remove_decode_signal(shared_ptr 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 : 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 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(*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 > View::get_visible_data() const +vector< shared_ptr > View::get_visible_data() const { // Make a set of all the visible data objects - set< shared_ptr > visible_data; + vector< shared_ptr > visible_data; for (const shared_ptr& 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 > View::get_visible_data() const pair View::get_time_extents() const { boost::optional left_time, right_time; - const set< shared_ptr > visible_data = get_visible_data(); - for (const shared_ptr& d : visible_data) { + + vector< shared_ptr > data; + if (signals_.size() == 0) + return make_pair(0, 0); + + data.push_back(signals_.front()->data()); + + for (const shared_ptr& d : data) { const vector< shared_ptr > segments = d->segments(); for (const shared_ptr& s : segments) { double samplerate = s->samplerate(); @@ -781,26 +855,6 @@ pair 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 View::cursors() const @@ -836,15 +904,17 @@ shared_ptr View::cursors() const return cursors_; } -void View::add_flag(const Timestamp& time) +shared_ptr View::add_flag(const Timestamp& time) { - flags_.push_back(make_shared(*this, time, - QString("%1").arg(next_flag_text_))); + shared_ptr flag = + make_shared(*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) @@ -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(*this, location)); } @@ -1182,9 +1254,13 @@ void View::update_scroll() const pair 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> trace_tree_items( + list_by_type()); + for (const shared_ptr& 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 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(); } diff --git a/pv/views/trace/view.hpp b/pv/views/trace/view.hpp index a6655d2f..d57acd91 100644 --- a/pv/views/trace/view.hpp +++ b/pv/views/trace/view.hpp @@ -28,6 +28,7 @@ #include #include +#include #include #include @@ -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 > signals() const; + vector< shared_ptr > signals() const; + + shared_ptr get_signal_by_signalbase(shared_ptr 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 > get_visible_data() const; + vector< shared_ptr > get_visible_data() const; pair 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 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 > 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 > signals_; #ifdef ENABLE_DECODE vector< shared_ptr > 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 > trigger_markers_; QWidget* hover_widget_; + TimeMarker* grabbed_widget_; QPoint hover_point_; shared_ptr signal_under_mouse_cursor_; uint16_t snap_distance_; diff --git a/pv/views/trace/viewitem.cpp b/pv/views/trace/viewitem.cpp index 2dd8ade8..f84080ec 100644 --- a/pv/views/trace/viewitem.cpp +++ b/pv/views/trace/viewitem.cpp @@ -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 diff --git a/pv/views/trace/viewitem.hpp b/pv/views/trace/viewitem.hpp index 5ce3bb60..423e75d2 100644 --- a/pv/views/trace/viewitem.hpp +++ b/pv/views/trace/viewitem.hpp @@ -22,6 +22,7 @@ #include +#include #include #include @@ -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(); diff --git a/pv/views/trace/viewport.cpp b/pv/views/trace/viewport.cpp index 83abb7b6..0c73ec49 100644 --- a/pv/views/trace/viewport.cpp +++ b/pv/views/trace/viewport.cpp @@ -30,6 +30,8 @@ #include #include +#include +#include #include @@ -67,8 +69,8 @@ shared_ptr Viewport::get_mouse_over_item(const QPoint &pt) void Viewport::item_hover(const shared_ptr &item, QPoint pos) { if (item && item->is_draggable(pos)) - setCursor(dynamic_pointer_cast(item) ? - Qt::SizeVerCursor : Qt::SizeHorCursor); + setCursor(dynamic_pointer_cast(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 > row_items(view_.list_by_type()); + vector< shared_ptr > row_items(view_.list_by_type()); assert(none_of(row_items.begin(), row_items.end(), - [](const shared_ptr &r) { return !r; })); + [](const shared_ptr &r) { return !r; })); stable_sort(row_items.begin(), row_items.end(), - [](const shared_ptr &a, const shared_ptr &b) { + [](const shared_ptr &a, const shared_ptr &b) { return a->drag_point(QRect()).y() < b->drag_point(QRect()).y(); }); const vector< shared_ptr > time_items(view_.time_items()); @@ -172,7 +177,11 @@ void Viewport::paintEvent(QPaintEvent*) [](const shared_ptr &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& r : row_items) + for (const shared_ptr& r : row_items) (r.get()->*(*paint_func))(p, row_pp); } diff --git a/pv/views/trace/viewwidget.cpp b/pv/views/trace/viewwidget.cpp index 73c361f4..5a85c15d 100644 --- a/pv/views/trace/viewwidget.cpp +++ b/pv/views/trace/viewwidget.cpp @@ -105,9 +105,9 @@ void ViewWidget::drag_items(const QPoint &delta) bool item_dragged = false; // Drag the row items - const vector< shared_ptr > row_items( - view_.list_by_type()); - for (const shared_ptr& r : row_items) + const vector< shared_ptr > row_items( + view_.list_by_type()); + for (const shared_ptr& 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() - + mouse_down_offset_.convert_to()) / 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(); } diff --git a/pv/views/trace/viewwidget.hpp b/pv/views/trace/viewwidget.hpp index f4928e67..427a8994 100644 --- a/pv/views/trace/viewwidget.hpp +++ b/pv/views/trace/viewwidget.hpp @@ -25,6 +25,8 @@ #include #include +#include + 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 mouse_down_item_; + + /// Keyboard modifiers that were active when mouse was last moved or clicked + Qt::KeyboardModifiers mouse_modifiers_; + bool item_dragging_; }; diff --git a/pv/views/viewbase.cpp b/pv/views/viewbase.cpp index 3c9bc8c0..24e4bb9a 100644 --- a/pv/views/viewbase.cpp +++ b/pv/views/viewbase.cpp @@ -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 > ViewBase::signalbases() const +vector< shared_ptr > ViewBase::signalbases() const { return signalbases_; } @@ -94,7 +107,7 @@ void ViewBase::clear_signalbases() void ViewBase::add_signalbase(const shared_ptr 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 signalbase) #ifdef ENABLE_DECODE void ViewBase::clear_decode_signals() { + decode_signals_.clear(); } void ViewBase::add_decode_signal(shared_ptr signal) { - (void)signal; + decode_signals_.push_back(signal); } void ViewBase::remove_decode_signal(shared_ptr signal) { - (void)signal; + decode_signals_.erase(std::remove_if( + decode_signals_.begin(), decode_signals_.end(), + [&](shared_ptr s) { return s == signal; }), + decode_signals_.end()); } #endif diff --git a/pv/views/viewbase.hpp b/pv/views/viewbase.hpp index b524c179..585dfa0c 100644 --- a/pv/views/viewbase.hpp +++ b/pv/views/viewbase.hpp @@ -26,6 +26,7 @@ #include #include +#include #include #include @@ -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 > signalbases() const; + vector< shared_ptr > 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 > signalbases_; + vector< shared_ptr > signalbases_; +#ifdef ENABLE_DECODE + vector< shared_ptr > decode_signals_; +#endif /// The ID of the currently displayed segment uint32_t current_segment_; diff --git a/pv/widgets/decodermenu.cpp b/pv/widgets/decodermenu.cpp index 28b54bab..505a1f67 100644 --- a/pv/widgets/decodermenu.cpp +++ b/pv/widgets/decodermenu.cpp @@ -26,43 +26,50 @@ 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(); + + srd_decoder *const dec = (srd_decoder*)((QAction*)action)->data().value(); assert(dec); decoder_selected(dec); diff --git a/pv/widgets/decodermenu.hpp b/pv/widgets/decodermenu.hpp index 2fe7cd2b..6ef4d2c6 100644 --- a/pv/widgets/decodermenu.hpp +++ b/pv/widgets/decodermenu.hpp @@ -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 index 00000000..efd862f9 --- /dev/null +++ b/pv/widgets/flowlayout.cpp @@ -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 + +#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(parent); + return pw->style()->pixelMetric(pm, nullptr, pw); + } else + return static_cast(parent)->spacing(); +} diff --git a/pv/widgets/flowlayout.hpp b/pv/widgets/flowlayout.hpp new file mode 100644 index 00000000..81407d1b --- /dev/null +++ b/pv/widgets/flowlayout.hpp @@ -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 +#include +#include +#include + +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 itemList; + int m_hSpace, m_vSpace; +}; + +#endif diff --git a/pv/widgets/importmenu.cpp b/pv/widgets/importmenu.cpp index 1a45aa62..e3034249 100644 --- a/pv/widgets/importmenu.cpp +++ b/pv/widgets/importmenu.cpp @@ -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, - QAction *open_action) : + vectoropen_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(); } diff --git a/pv/widgets/importmenu.hpp b/pv/widgets/importmenu.hpp index d1d5231a..c70962a4 100644 --- a/pv/widgets/importmenu.hpp +++ b/pv/widgets/importmenu.hpp @@ -26,6 +26,7 @@ #include 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 context, - QAction *open_action = nullptr); + vectoropen_actions = vector()); private Q_SLOTS: void on_action(QObject *action); diff --git a/pv/widgets/popup.cpp b/pv/widgets/popup.cpp index 13282cda..ec6d29c9 100644 --- a/pv/widgets/popup.cpp +++ b/pv/widgets/popup.cpp @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include #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_(), diff --git a/pv/widgets/popup.hpp b/pv/widgets/popup.hpp index 66f7f06f..879c5f9d 100644 --- a/pv/widgets/popup.hpp +++ b/pv/widgets/popup.hpp @@ -20,11 +20,29 @@ #ifndef PULSEVIEW_PV_WIDGETS_POPUP_HPP #define PULSEVIEW_PV_WIDGETS_POPUP_HPP +#include #include 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 diff --git a/pv/widgets/timestampspinbox.cpp b/pv/widgets/timestampspinbox.cpp index 21b3d0d7..fea8175e 100644 --- a/pv/widgets/timestampspinbox.cpp +++ b/pv/widgets/timestampspinbox.cpp @@ -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 diff --git a/pv/widgets/wellarray.hpp b/pv/widgets/wellarray.hpp index 4c67ca11..8b2e1ef2 100644 --- a/pv/widgets/wellarray.hpp +++ b/pv/widgets/wellarray.hpp @@ -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); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 300532e1..e3362ac8 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -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 index 00000000..103fa359 --- /dev/null +++ b/translations.qrc @@ -0,0 +1,5 @@ + + + l10n/de.qm + +