diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..95450f954 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,356 @@ +name: Build + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + workflow_dispatch: + +jobs: + linux: + name: Linux (deb + AppImage) + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential cmake git pkg-config \ + qt6-base-dev libqt6svg6-dev libgl1-mesa-dev libxkbcommon-dev libvulkan-dev \ + libglib2.0-dev zlib1g-dev libusb-1.0-0-dev libboost-dev libfftw3-dev \ + python3-dev libudev-dev dpkg-dev rsync patchelf + + - name: Configure + run: | + cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DCPACK_PACKAGE_CONTACT="pat.felix92@gmail.com" \ + -DCPACK_DEBIAN_PACKAGE_SHLIBDEPS=ON + + - name: Build + run: cmake --build build -j "$(nproc)" + + - name: Build .deb package + run: | + cd build + cpack -G DEB + + - name: Bundle Python (lib + stdlib) so the .deb doesn't depend on the host's Python + run: | + cd build + DEB="$(ls *.deb)" + rm -rf deb-extract + dpkg-deb -R "$DEB" deb-extract + + # DSView embeds Python (for its protocol decoders) and links + # libpython directly, so the binary has a hard DT_NEEDED on the + # exact libpythonX.Y.so the runner (Ubuntu 22.04, Python 3.10) + # built against, and the interpreter needs that same version's + # stdlib (encodings, etc.) to even bootstrap. Neither exists on + # newer distros (24.04 only ships Python 3.12), so the app would + # fail to start even though apt happily installed it. Fix: carry + # our own copy of both inside the package and point the binary at + # them via RPATH + a PYTHONHOME/PYTHONPATH wrapper, so it never + # relies on whatever Python happens to be on the host. + PYVER="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])')" + BIN="deb-extract/usr/bin/DSView" + PYSO="$(ldd "$BIN" | awk '/libpython/ {print $3}')" + BUNDLE="deb-extract/usr/lib/dsview" + mkdir -p "$BUNDLE" + cp -Lv "$PYSO" "$BUNDLE/" + patchelf --set-rpath '$ORIGIN/../lib/dsview' "$BIN" + + rsync -a \ + --exclude 'test' --exclude 'idlelib' --exclude 'tkinter' --exclude '__pycache__' \ + "/usr/lib/python${PYVER}/" "$BUNDLE/python${PYVER}/" + + mv "$BIN" "${BIN}.bin" + cat > "$BIN" </dev/null || true + + dpkg-deb --root-owner-group -b deb-extract "$DEB" + + - name: Upload .deb artifact + uses: actions/upload-artifact@v6 + with: + name: DSView-linux-deb + path: build/*.deb + + - name: Install into AppDir + run: | + rm -rf AppDir + DESTDIR="${GITHUB_WORKSPACE}/AppDir" cmake --install build + + - name: Download linuxdeploy + run: | + # linuxdeployqt (previously used here) has a regression in its + # current "continuous" build: run against DSView's .desktop file, it + # silently omits libQt6*.so from the bundle entirely (confirmed by + # rebuilding this exact sequence locally and inspecting the result - + # it parses Qt6Widgets/Qt6Gui/Qt6Core as dependencies via its own + # logging, but never copies them). The resulting "AppImage" only + # happened to run on machines that already had system Qt6 installed, + # which is indistinguishable from a working build unless tested on a + # clean system - reported as "AppImage doesn't run without Qt6 + # pre-installed". linuxdeploy + linuxdeploy-plugin-qt (actively + # maintained, purpose-built for Qt) does not share this bug - this + # was verified locally: a completely clean environment (env -i, no + # inherited LD_LIBRARY_PATH/QT_PLUGIN_PATH) successfully launched the + # resulting AppImage. + wget -q -O linuxdeploy \ + "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" + wget -q -O linuxdeploy-plugin-qt \ + "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" + chmod +x linuxdeploy linuxdeploy-plugin-qt + + - name: Build AppImage + run: | + export QMAKE="$(which qmake6)" + # linuxdeploy finds the "qt" plugin (linuxdeploy-plugin-qt) by + # searching PATH for a linuxdeploy-plugin-qt executable. + export PATH="${GITHUB_WORKSPACE}:${PATH}" + + # Deploy Qt/shared-lib dependencies into AppDir, but don't build the + # final .AppImage yet - we still need to bundle the Python stdlib + # (DSView embeds Python for its protocol decoders; the interpreter + # needs the stdlib data files it loads at runtime, e.g. the + # `encodings` module, which no Qt-focused deploy tool knows about). + ./linuxdeploy --appdir "${GITHUB_WORKSPACE}/AppDir" --plugin qt + + # Belt-and-suspenders: linuxdeploy-plugin-qt's own dependency scan + # already bundles libpython*.so (verified locally), but guard with + # an existence check rather than assume it always will - a plain + # `cp` would fail the step (source/dest resolving to the same file + # exits non-zero, and this workflow's steps run with `bash -e`) if + # it's already there. + PYSO="$(ldd "${GITHUB_WORKSPACE}/AppDir/usr/bin/DSView" | awk '/libpython/ {print $3}')" + if [ -n "${PYSO}" ] && [ ! -e "${GITHUB_WORKSPACE}/AppDir/usr/lib/$(basename "${PYSO}")" ]; then + cp -Lv "${PYSO}" "${GITHUB_WORKSPACE}/AppDir/usr/lib/" + fi + + PYVER="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])')" + PYLIBDIR="/usr/lib/python${PYVER}" + DEST="${GITHUB_WORKSPACE}/AppDir/usr/lib/python${PYVER}" + mkdir -p "${DEST}" + rsync -a \ + --exclude 'test' --exclude 'idlelib' --exclude 'tkinter' --exclude '__pycache__' \ + "${PYLIBDIR}/" "${DEST}/" + + # linuxdeploy's AppRun (a plain symlink to the binary for Qt6 apps - + # it relies on the $ORIGIN rpaths the qt plugin already set on every + # bundled library, not an LD_LIBRARY_PATH-setting script) only + # covers Qt/library resolution. Wrap it with our own AppRun that + # also points the bundled Python interpreter at the bundled stdlib, + # instead of falling back to whatever python3 (and version) happens + # to be on the host. exec transparently follows the AppRun.qt + # symlink, so this works the same regardless of what form it takes. + mv "${GITHUB_WORKSPACE}/AppDir/AppRun" "${GITHUB_WORKSPACE}/AppDir/AppRun.qt" + cat > "${GITHUB_WORKSPACE}/AppDir/AppRun" <- + git + zip + rsync + mingw-w64-x86_64-toolchain + mingw-w64-x86_64-cmake + mingw-w64-x86_64-ninja + mingw-w64-x86_64-qt6-base + mingw-w64-x86_64-qt6-svg + mingw-w64-x86_64-qt6-5compat + mingw-w64-x86_64-glib2 + mingw-w64-x86_64-zlib + mingw-w64-x86_64-libusb + mingw-w64-x86_64-boost + mingw-w64-x86_64-fftw + mingw-w64-x86_64-python + mingw-w64-x86_64-pkg-config + + - name: Configure + run: | + # Force CMake to find MSYS2's own mingw64 Python (whose runtime DLL + # we actually bundle below) instead of the native Windows Python + # that's preinstalled on the windows-latest runner (e.g. python314.dll + # under the hosted tool cache). If CMake picks that one up instead, + # DSView.exe ends up linked against a DLL that only exists on the CI + # runner, isn't found by the /mingw64/bin-only dependency bundling + # loop, and the packaged app fails to start with "python314.dll was + # not found". + cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=dsview-dist \ + -DPython3_ROOT_DIR=/mingw64 \ + -DPython3_FIND_STRATEGY=LOCATION \ + -DPython3_FIND_REGISTRY=NEVER \ + -DPython3_FIND_VIRTUALENV=STANDARD + + - name: Build + run: cmake --build build -j "$(nproc)" + + - name: Install + run: cmake --install build + + - name: Bundle runtime DLLs + run: | + # Qt loads platform/image/style plugins dynamically (not visible to + # ldd); without qwindows.dll in particular, the app can't even open + # a window. Qt's default search looks for a "platforms" etc. folder + # next to the executable. Copy these first so the ldd pass below + # also picks up libraries only the plugins need (e.g. Qt6Svg.dll, + # needed by the qsvg imageformat/iconengine plugins but not linked + # directly by DSView.exe itself). + for group in platforms styles imageformats iconengines; do + mkdir -p "dsview-dist/${group}" + cp -v /mingw64/share/qt6/plugins/${group}/*.dll "dsview-dist/${group}/" + done + + # DSView embeds Python (for its protocol decoders) via + # Py_InitializeEx(); without its own copy of the standard library + # (encodings, os.py, etc.) next to the exe, the interpreter can't + # bootstrap once libpython3.14.dll is copied out of MSYS2's + # /mingw64 prefix layout, and Py_FatalError() aborts the process + # silently before any window is shown. Bundle it under "pylib", + # which appcontrol.cpp points PYTHONHOME at via applicationDirPath(). + PYVER="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])')" + mkdir -p "dsview-dist/pylib" + rsync -a \ + --exclude 'test' --exclude 'idlelib' --exclude 'tkinter' --exclude '__pycache__' \ + "/mingw64/lib/python${PYVER}/" "dsview-dist/pylib/" + + # The install rules only place DSView.exe + data files; the MSYS2 + # toolchain/Qt6 runtime DLLs it's dynamically linked against still + # need to be copied in manually. Resolve the dependency closure with + # ldd and keep looping until a pass copies nothing new, since some + # transitive deps only show up once their dependents are present. + while :; do + new=0 + for dll in $(find dsview-dist -name '*.exe' -o -name '*.dll' \ + | xargs ldd 2>/dev/null \ + | grep -i '/mingw64/bin/' | awk '{print $3}' | sort -u); do + base="$(basename "$dll")" + if [ ! -f "dsview-dist/$base" ]; then + cp -v "$dll" dsview-dist/ + new=1 + fi + done + [ "$new" -eq 0 ] && break + done + + - name: Package as zip + run: | + cd dsview-dist + zip -r "${GITHUB_WORKSPACE}/DSView-windows-x86_64.zip" . + + - name: Upload artifact + uses: actions/upload-artifact@v6 + with: + name: DSView-windows + path: DSView-windows-x86_64.zip + + # Native Windows shell rather than the job-default MSYS2 bash for the + # Inno Setup steps below: iscc.exe/pnputil.exe are plain Windows tools, + # and MSYS2 bash's automatic POSIX<->Windows path conversion is an + # unnecessary complication for invoking them. + - name: Install Inno Setup + shell: pwsh + run: choco install innosetup -y --no-progress + + - name: Build Windows installer + shell: pwsh + run: | + # Read the app version from CMakeLists.txt instead of hand-duplicating + # it here, so the installer's version can't silently drift from the + # one DSView itself reports. + $major = (Select-String -Path CMakeLists.txt -Pattern 'DS_VERSION_MAJOR (\d+)').Matches[0].Groups[1].Value + $minor = (Select-String -Path CMakeLists.txt -Pattern 'DS_VERSION_MINOR (\d+)').Matches[0].Groups[1].Value + $micro = (Select-String -Path CMakeLists.txt -Pattern 'DS_VERSION_MICRO (\d+)').Matches[0].Groups[1].Value + $version = "$major.$minor.$micro" + & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" "installer\windows\dsview.iss" "/DMyAppVersion=$version" + + - name: Upload installer artifact + uses: actions/upload-artifact@v6 + with: + name: DSView-windows-installer + path: installer/windows/Output/DSView-windows-x86_64-setup.exe + + release: + name: Publish latest pre-release + needs: [linux, windows] + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts + merge-multiple: true + + - name: Publish "latest" pre-release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release delete latest --yes --cleanup-tag --repo "${GITHUB_REPOSITORY}" || true + gh release create latest artifacts/* \ + --repo "${GITHUB_REPOSITORY}" \ + --title "Latest build" \ + --notes "Automated build from commit ${GITHUB_SHA}" \ + --prerelease \ + --target "${GITHUB_SHA}" diff --git a/.gitignore b/.gitignore index 81fa4defb..07d032f6a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ autom4te.cache !cmake_modules CPackConfig.cmake CPackSourceConfig.cmake +_CPack_Packages +*.deb +*.AppImage cmake_install.cmake Makefile *.cxx @@ -48,7 +51,9 @@ moc_*.cpp moc_*.cpp_parameters DSView-prj -build* +build/* +build.dir* +.cache/* share .vscode qtpro diff --git a/CMakeLists.txt b/CMakeLists.txt index e5bf5b27a..11462b8a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,13 +31,13 @@ set(DS_TITLE DSView) set(DS_DESCRIPTION "A GUI for instruments of DreamSourceLab") set(DS_VERSION_MAJOR 1) -set(DS_VERSION_MINOR 4) -set(DS_VERSION_MICRO 2) +set(DS_VERSION_MINOR 5) +set(DS_VERSION_MICRO 0) set(DS_VERSION_STRING ${DS_VERSION_MAJOR}.${DS_VERSION_MINOR}.${DS_VERSION_MICRO}) configure_file ( - ${PROJECT_SOURCE_DIR}/DSView/config.h.in - ${PROJECT_BINARY_DIR}/DSView/config.h + ${PROJECT_SOURCE_DIR}/DSView/config.h.in + ${PROJECT_BINARY_DIR}/DSView/config.h ) #=============================================================================== @@ -46,13 +46,13 @@ configure_file ( include(FindPkgConfig) include(GNUInstallDirs) -list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMake") -#list(APPEND CMAKE_PREFIX_PATH "xxx.cmake find path") +list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMake") +#list(APPEND CMAKE_PREFIX_PATH "xxx.cmake find path") find_package(PkgConfig) if(NOT PKG_CONFIG_FOUND) - message(FATAL_ERROR "Please install pkg-config!") + message(FATAL_ERROR "Please install pkg-config!") endif() #=============================================================================== @@ -65,8 +65,8 @@ set(ENABLE_COTIRE FALSE) #Enable cotire set(ENABLE_TESTS FALSE) #Enable unit tests if(WIN32) - # Windows does not support UNIX signals. - set(ENABLE_SIGNALS FALSE) + # Windows does not support UNIX signals. + set(ENABLE_SIGNALS FALSE) endif() if(NOT CMAKE_BUILD_TYPE) @@ -91,7 +91,7 @@ include_directories( pkg_search_module(GLIB glib-2.0) if(NOT GLIB_FOUND) - message(FATAL_ERROR "Please install glib!") + message(FATAL_ERROR "Please install glib!") endif() message("----- glib-2.0:") @@ -103,7 +103,6 @@ link_directories(${GLIB_LIBDIR}) #=============================================================================== #= python3 #------------------------------------------------------------------------------- - find_package(Python3 COMPONENTS Development QUIET) if (Python3_FOUND) @@ -135,14 +134,14 @@ else() endif() endif() endif() - + #=============================================================================== #= FFTW #------------------------------------------------------------------------------- find_package(FFTW) if(NOT FFTW_FOUND) - message(FATAL_ERROR "Please install lib fftw!") + message(FATAL_ERROR "Please install lib fftw!") endif() message("----- FFTW:") @@ -156,7 +155,7 @@ include_directories(${FFTW_INCLUDE_DIRS}) find_package(libusb-1.0) if(NOT LIBUSB_1_FOUND) - message(FATAL_ERROR "Please install libusb!") + message(FATAL_ERROR "Please install libusb!") endif() message("----- libusb-1.0:") @@ -170,7 +169,7 @@ include_directories(${LIBUSB_1_INCLUDE_DIRS}) find_package(ZLIB QUIET) if(NOT ZLIB_FOUND) - message(FATAL_ERROR "Please install zlib!") + message(FATAL_ERROR "Please install zlib!") endif() message("----- zlib:") @@ -181,57 +180,45 @@ include_directories(${ZLIB_INCLUDE_DIRS}) #=============================================================================== #= Qt6 or Qt5 # -# Windows build only works with Qt5 for now due to WinExtras dependency, -# therefore do not yet attempt to build with Qt6. -# -# Otherwise prioritize Qt6 for better font rendering on OSX and better Wayland -# compatibility on Linux. +# Prioritize Qt6 for better font rendering on OSX, better Wayland compatibility +# on Linux, and to stay on the actively maintained Qt release on Windows. #------------------------------------------------------------------------------- - -if(NOT WIN32) - find_package(Qt6Core QUIET) -endif() +find_package(Qt6Core QUIET) if(Qt6Core_FOUND) - message("----- Qt6:") - message(STATUS " includes:" ${Qt6Core_INCLUDE_DIRS}) - find_package(Qt6Widgets REQUIRED) - find_package(Qt6Gui REQUIRED) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${Qt6Widgets_EXECUTABLE_COMPILE_FLAGS}") - set(QT_INCLUDE_DIRS ${Qt6Gui_INCLUDE_DIRS} ${Qt6Widgets_INCLUDE_DIRS}) - set(QT_LIBRARIES Qt6::Gui Qt6::Widgets) - if(WIN32) - find_package(Qt6Core5Compat REQUIRED) - add_definitions(${Qt6Core5Compat_DEFINITIONS}) - list(APPEND QT_LIBRARIES Qt6::Core5Compat) - list(APPEND QT_INCLUDE_DIRS ${Qt6Core5Compat_INCLUDE_DIRS}) - endif() - add_definitions(${Qt6Gui_DEFINITIONS} ${Qt6Widgets_DEFINITIONS}) + message("----- Qt6:") + message(STATUS " includes:" ${Qt6Core_INCLUDE_DIRS}) + find_package(Qt6Widgets REQUIRED) + find_package(Qt6Gui REQUIRED) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${Qt6Widgets_EXECUTABLE_COMPILE_FLAGS}") + set(QT_INCLUDE_DIRS ${Qt6Gui_INCLUDE_DIRS} ${Qt6Widgets_INCLUDE_DIRS}) + set(QT_LIBRARIES Qt6::Gui Qt6::Widgets) + if(WIN32) + find_package(Qt6Core5Compat REQUIRED) + add_definitions(${Qt6Core5Compat_DEFINITIONS}) + list(APPEND QT_LIBRARIES Qt6::Core5Compat) + list(APPEND QT_INCLUDE_DIRS ${Qt6Core5Compat_INCLUDE_DIRS}) + endif() + add_definitions(${Qt6Gui_DEFINITIONS} ${Qt6Widgets_DEFINITIONS}) else() - find_package(Qt5Core QUIET) + find_package(Qt5Core QUIET) endif() if(Qt5Core_FOUND) - message("----- Qt5:") - message(STATUS " includes:" ${Qt5Core_INCLUDE_DIRS}) - find_package(Qt5Widgets REQUIRED) - find_package(Qt5Gui REQUIRED) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${Qt5Widgets_EXECUTABLE_COMPILE_FLAGS}") - set(QT_INCLUDE_DIRS ${Qt5Gui_INCLUDE_DIRS} ${Qt5Widgets_INCLUDE_DIRS}) - set(QT_LIBRARIES Qt5::Gui Qt5::Widgets) - add_definitions(${Qt5Gui_DEFINITIONS} ${Qt5Widgets_DEFINITIONS}) - if(WIN32) - find_package(Qt5WinExtras REQUIRED) - add_definitions(${Qt5WinExtras_DEFINITIONS}) - list(APPEND QT_LIBRARIES Qt5::WinExtras) - list(APPEND QT_INCLUDE_DIRS ${Qt5WinExtras_INCLUDE_DIRS}) - endif() + message("----- Qt5:") + message(STATUS " includes:" ${Qt5Core_INCLUDE_DIRS}) + find_package(Qt5Widgets REQUIRED) + find_package(Qt5Gui REQUIRED) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${Qt5Widgets_EXECUTABLE_COMPILE_FLAGS}") + set(QT_INCLUDE_DIRS ${Qt5Gui_INCLUDE_DIRS} ${Qt5Widgets_INCLUDE_DIRS}) + set(QT_LIBRARIES Qt5::Gui Qt5::Widgets) + add_definitions(${Qt5Gui_DEFINITIONS} ${Qt5Widgets_DEFINITIONS}) endif() if(NOT Qt5Core_FOUND AND NOT Qt6Core_FOUND) - message("Error!The qt5 or qt6 can't find, if them has been installed, please append the install path to CMAKE_PREFIX_PATH, the command as:") - message("list(APPEND CMAKE_PREFIX_PATH \"xxx install path\")" ) - message(FATAL_ERROR "Can't find Qt5 or Qt6!") + message("Error!The qt5 or qt6 can't find, if them has been installed, please append the install path to CMAKE_PREFIX_PATH, the command as:") + message("list(APPEND CMAKE_PREFIX_PATH \"xxx install path\")" ) + message(FATAL_ERROR "Can't find Qt5 or Qt6!") endif() #=============================================================================== @@ -240,7 +227,7 @@ endif() find_package(Boost 1.42 QUIET) if(NOT Boost_FOUND) - message(FATAL_ERROR "Please install boost!") + message(FATAL_ERROR "Please install boost!") endif() message("----- boost:") @@ -250,7 +237,6 @@ include_directories(${Boost_INCLUDE_DIRS}) #=============================================================================== #= Dependencies #------------------------------------------------------------------------------- - find_package(Threads) #=============================================================================== @@ -321,8 +307,11 @@ set(DSView_SOURCES DSView/pv/dialogs/protocollist.cpp DSView/pv/dialogs/protocolexp.cpp DSView/pv/dialogs/fftoptions.cpp + DSView/pv/dialogs/dsohistogram.cpp + DSView/pv/dialogs/chanmeasure.cpp + DSView/pv/dialogs/refoptions.cpp DSView/pv/data/mathstack.cpp - DSView/pv/view/mathtrace.cpp + DSView/pv/view/mathtrace.cpp DSView/pv/toolbars/titlebar.cpp DSView/pv/mainframe.cpp DSView/pv/widgets/border.cpp @@ -438,12 +427,15 @@ set(DSView_HEADERS DSView/pv/view/spectrumtrace.h DSView/pv/data/spectrumstack.h DSView/pv/dialogs/mathoptions.h + DSView/pv/dialogs/dsohistogram.h + DSView/pv/dialogs/chanmeasure.h + DSView/pv/dialogs/refoptions.h DSView/pv/dialogs/regionoptions.h DSView/pv/view/xcursor.h DSView/pv/view/signal.h DSView/pv/view/logicsignal.h DSView/pv/view/analogsignal.h - DSView/pv/view/dsosignal.h + DSView/pv/view/dsosignal.h DSView/pv/dock/protocoldock.h DSView/pv/data/decoderstack.h DSView/pv/view/decodetrace.h @@ -471,7 +463,7 @@ set(DSView_HEADERS DSView/pv/ui/fn.h DSView/pv/ui/xtoolbutton.h ) - + #=============================================================================== #= libsigrok4DSL source #------------------------------------------------------------------------------- @@ -514,7 +506,7 @@ set(libsigrok4DSL_HEADERS libsigrok4DSL/hardware/DSL/command.h libsigrok4DSL/hardware/DSL/dsl.h ) - + #=============================================================================== #= libsigrokdecode4DSL source #------------------------------------------------------------------------------- @@ -572,10 +564,10 @@ set(DSView_RESOURCES if(WIN32) - # Use the DSView icon for the DSView.exe executable. - set(CMAKE_RC_COMPILE_OBJECT "${CMAKE_RC_COMPILER} -O coff -I${CMAKE_CURRENT_SOURCE_DIR} ") - enable_language(RC) - # app icon + # Use the DSView icon for the DSView.exe executable. + set(CMAKE_RC_COMPILE_OBJECT "${CMAKE_RC_COMPILER} -O coff -I${CMAKE_CURRENT_SOURCE_DIR} ") + enable_language(RC) + # app icon list(APPEND DSView_SOURCES applogo.rc) list(APPEND DSView_SOURCES DSView/pv/winnativewidget.cpp @@ -588,15 +580,15 @@ if(WIN32) endif() if(Qt5Core_FOUND) - qt5_wrap_cpp(DSView_HEADERS_MOC ${DSView_HEADERS}) - qt5_wrap_ui(DSView_FORMS_HEADERS ${DSView_FORMS}) - qt5_add_resources(DSView_RESOURCES_RCC ${DSView_RESOURCES}) + qt5_wrap_cpp(DSView_HEADERS_MOC ${DSView_HEADERS}) + qt5_wrap_ui(DSView_FORMS_HEADERS ${DSView_FORMS}) + qt5_add_resources(DSView_RESOURCES_RCC ${DSView_RESOURCES}) endif() if(Qt6Core_FOUND) - qt6_wrap_cpp(DSView_HEADERS_MOC ${DSView_HEADERS}) - qt6_wrap_ui(DSView_FORMS_HEADERS ${DSView_FORMS}) - qt6_add_resources(DSView_RESOURCES_RCC ${DSView_RESOURCES}) + qt6_wrap_cpp(DSView_HEADERS_MOC ${DSView_HEADERS}) + qt6_wrap_ui(DSView_FORMS_HEADERS ${DSView_FORMS}) + qt6_add_resources(DSView_RESOURCES_RCC ${DSView_RESOURCES}) endif() @@ -612,10 +604,10 @@ set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99") include_directories( - ${CMAKE_CURRENT_BINARY_DIR} - ${CMAKE_CURRENT_SOURCE_DIR} - ${Boost_INCLUDE_DIRS} - ${QT_INCLUDE_DIRS} + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_CURRENT_SOURCE_DIR} + ${Boost_INCLUDE_DIRS} + ${QT_INCLUDE_DIRS} ) #=============================================================================== @@ -631,30 +623,29 @@ add_compile_options(-O3) #= Linker Configuration #------------------------------------------------------------------------------- - set(DSVIEW_LINK_LIBS - -lz - -lglib-2.0 - ${CMAKE_THREAD_LIBS_INIT} - ${QT_LIBRARIES} - ${LIBUSB_1_LIBRARIES} - ${FFTW_LIBRARIES} - ${PY_LIB} + -lz + -lglib-2.0 + ${CMAKE_THREAD_LIBS_INIT} + ${QT_LIBRARIES} + ${LIBUSB_1_LIBRARIES} + ${FFTW_LIBRARIES} + ${PY_LIB} ) if(WIN32) - # Workaround for a MinGW linking issue. - list(APPEND PULSEVIEW_LINK_LIBS "-llzma -llcms2") + # Workaround for a MinGW linking issue. + list(APPEND PULSEVIEW_LINK_LIBS "-llzma -llcms2") endif() add_executable(${PROJECT_NAME} - ${common_SOURCES} - ${DSView_SOURCES} - ${DSView_HEADERS_MOC} - ${DSView_FORMS_HEADERS} - ${DSView_RESOURCES_RCC} - ${libsigrok4DSL_SOURCES} - ${libsigrokdecode4DSL_SOURCES} + ${common_SOURCES} + ${DSView_SOURCES} + ${DSView_HEADERS_MOC} + ${DSView_FORMS_HEADERS} + ${DSView_RESOURCES_RCC} + ${libsigrok4DSL_SOURCES} + ${libsigrokdecode4DSL_SOURCES} ) target_link_libraries(${PROJECT_NAME} ${DSVIEW_LINK_LIBS}) @@ -665,8 +656,8 @@ set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "-mwindows") endif() if(ENABLE_COTIRE) - include(cotire) - cotire(${PROJECT_NAME}) + include(cotire) + cotire(${PROJECT_NAME}) endif() message(STATUS "Output dir: ${CMAKE_CURRENT_SOURCE_DIR}/build.dir") @@ -677,54 +668,65 @@ set(EXECUTABLE_OUTPUT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/build.dir") #------------------------------------------------------------------------------- if(WIN32) - # Settings for NSIS to create a Windows installer - set(CPACK_GENERATOR "NSIS") - set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") - set(CPACK_PACKAGE_EXECUTABLES DSView DSView) - set(CPACK_CREATE_DESKTOP_LINKS DSView) - set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/logo-win.ico") - set(CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/logo-win.ico") - set(CPACK_PACKAGE_VENDOR "DreamSourceLab") - set(CPACK_NSIS_INSTALLED_ICON_NAME "DSView.exe") - install(TARGETS ${PROJECT_NAME} DESTINATION .) - install(DIRECTORY DSView/res DESTINATION .) - install(DIRECTORY DSView/demo DESTINATION .) - install(DIRECTORY lang DESTINATION .) - install(DIRECTORY libsigrokdecode4DSL/decoders - DESTINATION . - PATTERN "__pycache__" EXCLUDE) - install(DIRECTORY cross-compile-windows/python-setup/python-dist/. DESTINATION .) + # Settings for NSIS to create a Windows installer + set(CPACK_GENERATOR "NSIS") + set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") + set(CPACK_PACKAGE_EXECUTABLES DSView DSView) + set(CPACK_CREATE_DESKTOP_LINKS DSView) + set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/logo-win.ico") + set(CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/logo-win.ico") + set(CPACK_PACKAGE_VENDOR "DreamSourceLab") + set(CPACK_NSIS_INSTALLED_ICON_NAME "DSView.exe") + install(TARGETS ${PROJECT_NAME} DESTINATION .) + install(DIRECTORY DSView/res DESTINATION .) + install(DIRECTORY DSView/demo DESTINATION .) + install(DIRECTORY lang DESTINATION .) + install(DIRECTORY libsigrokdecode4DSL/decoders + DESTINATION . + PATTERN "__pycache__" EXCLUDE) + if(IS_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/cross-compile-windows/python-setup/python-dist) + install(DIRECTORY cross-compile-windows/python-setup/python-dist/. DESTINATION .) + endif() else() - # Install the executable. - install(TARGETS ${PROJECT_NAME} DESTINATION bin) - install(DIRECTORY DSView/res DESTINATION share/DSView) - install(DIRECTORY DSView/demo DESTINATION share/DSView) - install(FILES DSView/icons/logo.svg DESTINATION share/DSView RENAME logo.svg) - install(FILES DSView/icons/logo.svg DESTINATION share/icons/hicolor/scalable/apps RENAME dsview.svg) - install(FILES DSView/icons/logo.svg DESTINATION share/pixmaps RENAME dsview.svg) - - if(CMAKE_SYSTEM_NAME MATCHES "Linux") - install(FILES DSView/DSView.desktop DESTINATION /usr/share/applications RENAME dsview.desktop) - - add_compile_definitions(_DEFAULT_SOURCE) - - if(IS_DIRECTORY /usr/lib/udev/rules.d) - install(FILES DSView/DreamSourceLab.rules DESTINATION /usr/lib/udev/rules.d RENAME 60-dreamsourcelab.rules) - elseif(IS_DIRECTORY /lib/udev/rules.d) - install(FILES DSView/DreamSourceLab.rules DESTINATION /lib/udev/rules.d RENAME 60-dreamsourcelab.rules) - elseif(IS_DIRECTORY /etc/udev/rules.d) - install(FILES DSView/DreamSourceLab.rules DESTINATION /etc/udev/rules.d RENAME 60-dreamsourcelab.rules) - endif() - - endif() - - install(FILES NEWS25 DESTINATION share/DSView RENAME NEWS25) - install(FILES NEWS31 DESTINATION share/DSView RENAME NEWS31) - install(FILES ug25.pdf DESTINATION share/DSView RENAME ug25.pdf) - install(FILES ug31.pdf DESTINATION share/DSView RENAME ug31.pdf) - - install(DIRECTORY libsigrokdecode4DSL/decoders DESTINATION share/libsigrokdecode4DSL) - install(DIRECTORY lang DESTINATION share/DSView) + # Install the executable. + install(TARGETS ${PROJECT_NAME} DESTINATION bin) + install(DIRECTORY DSView/res DESTINATION share/DSView) + install(DIRECTORY DSView/demo DESTINATION share/DSView) + install(FILES DSView/icons/logo.svg DESTINATION share/DSView RENAME logo.svg) + install(FILES DSView/icons/logo.svg DESTINATION share/icons/hicolor/scalable/apps RENAME dsview.svg) + install(FILES DSView/icons/logo.svg DESTINATION share/pixmaps RENAME dsview.svg) + + if(CMAKE_SYSTEM_NAME MATCHES "Linux") + install(FILES DSView/DSView.desktop DESTINATION /usr/share/applications RENAME dsview.desktop) + + add_compile_definitions(_DEFAULT_SOURCE) + + if(IS_DIRECTORY /usr/lib/udev/rules.d) + install(FILES DSView/DreamSourceLab.rules DESTINATION /usr/lib/udev/rules.d RENAME 60-dreamsourcelab.rules) + elseif(IS_DIRECTORY /lib/udev/rules.d) + install(FILES DSView/DreamSourceLab.rules DESTINATION /lib/udev/rules.d RENAME 60-dreamsourcelab.rules) + elseif(IS_DIRECTORY /etc/udev/rules.d) + install(FILES DSView/DreamSourceLab.rules DESTINATION /etc/udev/rules.d RENAME 60-dreamsourcelab.rules) + endif() + + # Also ship a copy alongside the app itself (share/DSView, same as the + # other resources installed below), so it ends up inside the AppImage + # too - unlike the install() rules above, AppImage packaging has no + # way to place files outside its own AppDir, so this is the only copy + # an AppImage user has access to. MainFrame::show_driver_hint_once() + # looks for it here to give AppImage users a one-time copy-paste fix + # when the system-wide rule (installed above) isn't present. + install(FILES DSView/DreamSourceLab.rules DESTINATION share/DSView) + + endif() + + install(FILES NEWS25 DESTINATION share/DSView RENAME NEWS25) + install(FILES NEWS31 DESTINATION share/DSView RENAME NEWS31) + install(FILES ug25.pdf DESTINATION share/DSView RENAME ug25.pdf) + install(FILES ug31.pdf DESTINATION share/DSView RENAME ug31.pdf) + + install(DIRECTORY libsigrokdecode4DSL/decoders DESTINATION share/libsigrokdecode4DSL) + install(DIRECTORY lang DESTINATION share/DSView) endif() #=============================================================================== @@ -738,9 +740,19 @@ set(CPACK_PACKAGE_DESCRIPTION_FILE ${CMAKE_CURRENT_SOURCE_DIR}/DSView/README) set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_CURRENT_SOURCE_DIR}/DSView/COPYING) set(CPACK_SOURCE_IGNORE_FILES ${CMAKE_CURRENT_BINARY_DIR} ".gitignore" ".git") set(CPACK_SOURCE_PACKAGE_FILE_NAME - "${CMAKE_PROJECT_NAME}-${DS_VERSION_MAJOR}.${DS_VERSION_MINOR}.${DS_VERSION_MICRO}") + "${CMAKE_PROJECT_NAME}-${DS_VERSION_MAJOR}.${DS_VERSION_MINOR}.${DS_VERSION_MICRO}") set(CPACK_SOURCE_GENERATOR "TGZ") +# Reload udev immediately after installing/removing the .deb, so the +# 60-dreamsourcelab.rules permission fix (see the udev install() rules above) +# takes effect without requiring a reboot or manual `udevadm` invocation. +# CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA is ignored by every generator except DEB, +# so this is safe to set unconditionally. +set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA + "${CMAKE_CURRENT_SOURCE_DIR}/DSView/packaging/debian/postinst" + "${CMAKE_CURRENT_SOURCE_DIR}/DSView/packaging/debian/postrm" +) + include(CPack) #=============================================================================== @@ -748,9 +760,9 @@ include(CPack) #------------------------------------------------------------------------------- if(ENABLE_TESTS) - add_subdirectory(test) - enable_testing() - add_test(test ${CMAKE_CURRENT_BINARY_DIR}/DSView/test/DSView-test) + add_subdirectory(test) + enable_testing() + add_test(test ${CMAKE_CURRENT_BINARY_DIR}/DSView/test/DSView-test) endif(ENABLE_TESTS) diff --git a/DSView/DSView.qrc b/DSView/DSView.qrc index 222f82110..1e19956e0 100644 --- a/DSView/DSView.qrc +++ b/DSView/DSView.qrc @@ -36,6 +36,7 @@ icons/demo.svg icons/dark/moder.svg icons/dark/modes.svg + icons/dark/dso-split.svg icons/dark/trigger.svg icons/light/about.svg icons/light/add.svg @@ -104,10 +105,12 @@ icons/Chinese.svg icons/data.svg icons/English.svg + icons/German.svg icons/usb2.svg icons/usb3.svg icons/light/moder.svg icons/light/modes.svg + icons/light/dso-split.svg icons/math.svg icons/lissajous.svg icons/dark/single.svg diff --git a/DSView/icons/German.svg b/DSView/icons/German.svg new file mode 100644 index 000000000..d10c8174b --- /dev/null +++ b/DSView/icons/German.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/DSView/icons/dark/dso-split.svg b/DSView/icons/dark/dso-split.svg new file mode 100644 index 000000000..e3eb53b3d --- /dev/null +++ b/DSView/icons/dark/dso-split.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/DSView/icons/light/dso-split.svg b/DSView/icons/light/dso-split.svg new file mode 100644 index 000000000..a59f88935 --- /dev/null +++ b/DSView/icons/light/dso-split.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/DSView/main.cpp b/DSView/main.cpp index f8fe25212..ee60bc705 100644 --- a/DSView/main.cpp +++ b/DSView/main.cpp @@ -159,11 +159,15 @@ bool bHighScale = true; bHighScale = false; } #endif +#if QT_VERSION < QT_VERSION_CHECK(6,0,0) if (bHighScale){ QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); } -#endif +#else + (void)bHighScale; +#endif +#endif //----------------------init app QApplication a(argcFinal, argvFinal); diff --git a/DSView/packaging/debian/postinst b/DSView/packaging/debian/postinst new file mode 100755 index 000000000..32d5434e0 --- /dev/null +++ b/DSView/packaging/debian/postinst @@ -0,0 +1,15 @@ +#!/bin/sh +# Reload udev after installing 60-dreamsourcelab.rules (see CMakeLists.txt), +# so the device permission fix takes effect immediately - without this, a +# freshly plugged-in DreamSourceLab device would still be denied access by +# libusb until the next reboot or a manual `udevadm control --reload-rules`. +set -e + +if [ "$1" = "configure" ]; then + if command -v udevadm >/dev/null 2>&1; then + udevadm control --reload-rules || true + udevadm trigger --subsystem-match=usb || true + fi +fi + +exit 0 diff --git a/DSView/packaging/debian/postrm b/DSView/packaging/debian/postrm new file mode 100755 index 000000000..a5439ef3b --- /dev/null +++ b/DSView/packaging/debian/postrm @@ -0,0 +1,13 @@ +#!/bin/sh +# Reload udev after removing 60-dreamsourcelab.rules, so a DreamSourceLab +# device's permissions revert immediately rather than only on next reboot. +set -e + +if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then + if command -v udevadm >/dev/null 2>&1; then + udevadm control --reload-rules || true + udevadm trigger --subsystem-match=usb || true + fi +fi + +exit 0 diff --git a/DSView/pv/appcontrol.cpp b/DSView/pv/appcontrol.cpp index d5cb559b5..df29b1eea 100644 --- a/DSView/pv/appcontrol.cpp +++ b/DSView/pv/appcontrol.cpp @@ -94,15 +94,35 @@ bool AppControl::Init() srd_log_set_context(dsv_log_context()); -#if defined(_WIN32) && defined(DEBUG_INFO) - //able run debug with qtcreator - QString pythonHome = "c:/python"; +#if defined(_WIN32) + // The packaged app carries its own copy of the Python standard library + // next to DSView.exe (see the "pylib" folder produced by the Windows CI + // build), since CPython's own relative-path auto-detection isn't + // reliable once the interpreter DLL is copied out of its original + // MSYS2/mingw64 prefix layout into a flat distribution folder. Without + // PYTHONHOME pointing at it, Py_InitializeEx() fails to find the stdlib + // and calls Py_FatalError(), which aborts the process before any window + // is shown and before any of our own logging can run. + QString pythonHome = QCoreApplication::applicationDirPath() + "/pylib"; QDir pydir; if (pydir.exists(pythonHome)){ const wchar_t *pyhome = reinterpret_cast(pythonHome.utf16()); srd_set_python_home(pyhome); + + // PYTHONHOME alone isn't enough: MSYS2's Python is built with a + // Unix-style prefix layout, so Py_SetPythonHome() makes CPython look + // for the stdlib under "/lib/pythonX.Y/...", not directly + // inside "/". Our bundled copy sits flat in "pylib/" (its + // encodings/, os.py etc. are direct children), so point PYTHONPATH + // straight at it too - this is read during interpreter bootstrap, + // before Py_SetPythonHome's own (mismatched) landmark search would + // otherwise fail to find "encodings" and abort the process. + QString libDynload = pythonHome + "/lib-dynload"; + QString pythonPath = pythonHome; + if (pydir.exists(libDynload)) + pythonPath += ";" + libDynload; + qputenv("PYTHONPATH", pythonPath.toUtf8()); } - #endif //the python script path of decoder diff --git a/DSView/pv/config/appconfig.cpp b/DSView/pv/config/appconfig.cpp index faf7fdd88..3a51521ea 100644 --- a/DSView/pv/config/appconfig.cpp +++ b/DSView/pv/config/appconfig.cpp @@ -1,7 +1,7 @@ /* * This file is part of the DSView project. * DSView is based on PulseView. - * + * * Copyright (C) 2021 DreamSourceLab * * This program is free software; you can redistribute it and/or modify @@ -19,15 +19,15 @@ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -#include "appconfig.h" +#include "appconfig.h" #include #include #include -#include +#include #include #include #include "../log.h" - + #define MAX_PROTOCOL_FORMAT_LIST 15 StringPair::StringPair(const std::string &key, const std::string &value) @@ -44,10 +44,10 @@ static QString FormatArrayToString(std::vector &protocolFormats) for (StringPair &o : protocolFormats){ if (!str.isEmpty()){ str += ";"; - } + } str += o.m_key.c_str(); str += "="; - str += o.m_value.c_str(); + str += o.m_value.c_str(); } return str; @@ -99,7 +99,7 @@ static void setFiled(const char *key, QSettings &st, bool f){ static void getFiled(const char *key, QSettings &st, float &f, float dv) { - f = st.value(key, dv).toInt(); + f = st.value(key, dv).toFloat(); } static void setFiled(const char *key, QSettings &st, float f) @@ -110,7 +110,7 @@ static void setFiled(const char *key, QSettings &st, float f) ///------ app static void _loadApp(AppOptions &o, QSettings &st) { - st.beginGroup("Application"); + st.beginGroup("Application"); getFiled("quickScroll", st, o.quickScroll, true); getFiled("warnofMultiTrig", st, o.warnofMultiTrig, true); getFiled("originalData", st, o.originalData, false); @@ -124,12 +124,17 @@ static void _loadApp(AppOptions &o, QSettings &st) getFiled("fontSize", st, o.fontSize, 9.0); getFiled("autoScrollLatestData", st, o.autoScrollLatestData, true); getFiled("verticalScrollIsZoom", st, o.verticalScrollIsZoom, true); + getFiled("traceHeightFactor", st, o.traceHeightFactor, 1.0); getFiled("version", st, o.version, 1); getFiled("rulerTimeUnits", st, o.rulerTimeUnits, "Time"); getFiled("antialias", st, o.antialias, true); getFiled("decoderDynamicFontWidth", st, o.decoderDynamicFontWidth, false); getFiled("maxDecoderFontWidthPercent", st, o.maxDecoderFontWidthPercent, 125); getFiled("minDecoderFontWidthPercent", st, o.minDecoderFontWidthPercent, 75); + getFiled("dontAskSaveOnExit", st, o.dontAskSaveOnExit, false); + getFiled("logicSignalLineWidth", st, o.logicSignalLineWidth, 1.0f); + getFiled("logicChannelDivider", st, o.logicChannelDivider, true); + getFiled("dsoSplitChannels", st, o.dsoSplitChannels, false); o.warnofMultiTrig = true; @@ -147,7 +152,17 @@ static void _loadApp(AppOptions &o, QSettings &st) { o.fontSize = (maxSize + minSize) / 2; } - + + if (o.traceHeightFactor < 0.1 || o.traceHeightFactor > 20.0) + { + o.traceHeightFactor = 1.0; + } + + if (o.logicSignalLineWidth < 1.0f || o.logicSignalLineWidth > 4.0f) + { + o.logicSignalLineWidth = 1.0f; + } + st.endGroup(); } @@ -167,16 +182,21 @@ static void _saveApp(AppOptions &o, QSettings &st) setFiled("fontSize", st, o.fontSize); setFiled("autoScrollLatestData", st, o.autoScrollLatestData); setFiled("verticalScrollIsZoom", st, o.verticalScrollIsZoom); + setFiled("traceHeightFactor", st, o.traceHeightFactor); setFiled("version", st, APP_CONFIG_VERSION); setFiled("rulerTimeUnits", st, o.rulerTimeUnits); setFiled("antialias", st, o.antialias); setFiled("decoderDynamicFontWidth", st, o.decoderDynamicFontWidth); setFiled("maxDecoderFontWidthPercent", st, o.maxDecoderFontWidthPercent); setFiled("minDecoderFontWidthPercent", st, o.minDecoderFontWidthPercent); + setFiled("dontAskSaveOnExit", st, o.dontAskSaveOnExit); + setFiled("logicSignalLineWidth", st, o.logicSignalLineWidth); + setFiled("logicChannelDivider", st, o.logicChannelDivider); + setFiled("dsoSplitChannels", st, o.dsoSplitChannels); QString fmt = FormatArrayToString(o.m_protocolFormats); setFiled("protocalFormats", st, fmt); - st.endGroup(); + st.endGroup(); } //-----frame @@ -203,10 +223,10 @@ static void _saveDockOptions(DockOptions &o, QSettings &st, const char *group) static void _loadFrame(FrameOptions &o, QSettings &st) { - st.beginGroup("MainFrame"); + st.beginGroup("MainFrame"); getFiled("style", st, o.style, THEME_STYLE_DARK); getFiled("language", st, o.language, -1); - getFiled("isMax", st, o.isMax, false); + getFiled("isMax", st, o.isMax, false); getFiled("left", st, o.left, 0); getFiled("top", st, o.top, 0); getFiled("right", st, o.right, 0); @@ -224,14 +244,16 @@ static void _loadFrame(FrameOptions &o, QSettings &st) o.windowState = st.value("windowState", QByteArray()).toByteArray(); st.endGroup(); - if (o.language == -1 || (o.language != LAN_CN && o.language != LAN_EN)){ + if (o.language == -1 || (o.language != LAN_CN && o.language != LAN_EN && o.language != LAN_DE)){ //get local language QLocale locale; if (QLocale::languageToString(locale.language()) == "Chinese") - o.language = LAN_CN; + o.language = LAN_CN; + else if (QLocale::languageToString(locale.language()) == "German") + o.language = LAN_DE; else - o.language = LAN_EN; + o.language = LAN_EN; } } @@ -240,7 +262,7 @@ static void _saveFrame(FrameOptions &o, QSettings &st) st.beginGroup("MainFrame"); setFiled("style", st, o.style); setFiled("language", st, o.language); - setFiled("isMax", st, o.isMax); + setFiled("isMax", st, o.isMax); setFiled("left", st, o.left); setFiled("top", st, o.top); setFiled("right", st, o.right); @@ -251,12 +273,12 @@ static void _saveFrame(FrameOptions &o, QSettings &st) setFiled("oy", st, o.oy); setFiled("displayName", st, o.displayName); - st.setValue("windowState", o.windowState); + st.setValue("windowState", o.windowState); _saveDockOptions(o._logicDock, st, "LOGIC_DOCK"); _saveDockOptions(o._analogDock, st, "ANALOG_DOCK"); _saveDockOptions(o._dsoDock, st, "DSO_DOCK"); - + st.endGroup(); } @@ -264,28 +286,30 @@ static void _saveFrame(FrameOptions &o, QSettings &st) static void _loadHistory(UserHistory &o, QSettings &st) { st.beginGroup("UserHistory"); - getFiled("exportDir", st, o.exportDir, ""); - getFiled("saveDir", st, o.saveDir, ""); + getFiled("exportDir", st, o.exportDir, ""); + getFiled("saveDir", st, o.saveDir, ""); getFiled("showDocuments", st, o.showDocuments, true); - getFiled("screenShotPath", st, o.screenShotPath, ""); - getFiled("sessionDir", st, o.sessionDir, ""); - getFiled("openDir", st, o.openDir, ""); - getFiled("protocolExportPath", st, o.protocolExportPath, ""); - getFiled("exportFormat", st, o.exportFormat, ""); + getFiled("screenShotPath", st, o.screenShotPath, ""); + getFiled("sessionDir", st, o.sessionDir, ""); + getFiled("openDir", st, o.openDir, ""); + getFiled("protocolExportPath", st, o.protocolExportPath, ""); + getFiled("exportFormat", st, o.exportFormat, ""); + getFiled("showDriverHint", st, o.showDriverHint, true); st.endGroup(); } - + static void _saveHistory(UserHistory &o, QSettings &st) { st.beginGroup("UserHistory"); - setFiled("exportDir", st, o.exportDir); - setFiled("saveDir", st, o.saveDir); - setFiled("showDocuments", st, o.showDocuments); - setFiled("screenShotPath", st, o.screenShotPath); - setFiled("sessionDir", st, o.sessionDir); - setFiled("openDir", st, o.openDir); + setFiled("exportDir", st, o.exportDir); + setFiled("saveDir", st, o.saveDir); + setFiled("showDocuments", st, o.showDocuments); + setFiled("screenShotPath", st, o.screenShotPath); + setFiled("sessionDir", st, o.sessionDir); + setFiled("openDir", st, o.openDir); setFiled("protocolExportPath", st, o.protocolExportPath); - setFiled("exportFormat", st, o.exportFormat); + setFiled("exportFormat", st, o.exportFormat); + setFiled("showDriverHint", st, o.showDriverHint); st.endGroup(); } @@ -333,10 +357,10 @@ static void _saveFont(FontOptions &o, QSettings &st) //------------AppConfig AppConfig::AppConfig() -{ +{ } -AppConfig::AppConfig(AppConfig &o) +AppConfig::AppConfig(AppConfig &o) { (void)o; } @@ -347,15 +371,12 @@ AppConfig::~AppConfig() AppConfig& AppConfig::Instance() { - static AppConfig *ins = NULL; - if (ins == NULL){ - ins = new AppConfig(); - } - return *ins; + static AppConfig ins; + return ins; } void AppConfig::LoadAll() -{ +{ QSettings st(QApplication::organizationName(), QApplication::applicationName()); _loadApp(appOptions, st); _loadHistory(userHistory, st); @@ -390,7 +411,7 @@ void AppConfig::SetProtocolFormat(const std::string &protocolName, const std::st o.m_value = value; bChange = true; break; - } + } } if (!bChange) @@ -414,13 +435,27 @@ void AppConfig::SetProtocolFormat(const std::string &protocolName, const std::st std::string AppConfig::GetProtocolFormat(const std::string &protocolName) { for (StringPair &o : appOptions.m_protocolFormats){ - if (o.m_key == protocolName){ + if (o.m_key == protocolName){ return o.m_value; } } return ""; } +float AppConfig::GetTraceFontSize() +{ + float minSize = 0; + float maxSize = 0; + GetFontSizeRange(&minSize, &maxSize); + + float size = appOptions.fontSize; + if (size < minSize) + size = minSize; + if (size > maxSize) + size = maxSize; + return size; +} + void AppConfig::GetFontSizeRange(float *minSize, float *maxSize) { assert(minSize); @@ -433,7 +468,7 @@ void AppConfig::GetFontSizeRange(float *minSize, float *maxSize) #ifdef Q_OS_LINUX *minSize = 8; - *maxSize = 14; + *maxSize = 16; #endif #ifdef Q_OS_DARWIN @@ -444,7 +479,11 @@ void AppConfig::GetFontSizeRange(float *minSize, float *maxSize) bool AppConfig::IsDarkStyle() { - if (frameOptions.style == THEME_STYLE_DARK){ + // Frappe is Catppuccin's dark flavor - treat it like Dark for anything + // that only distinguishes light/dark (icon set selection, background + // luminosity-dependent painting, etc), rather than the exact color + // scheme in use. + if (frameOptions.style == THEME_STYLE_DARK || frameOptions.style == THEME_STYLE_FRAPPE){ return true; } return false; @@ -452,7 +491,13 @@ bool AppConfig::IsDarkStyle() QColor AppConfig::GetStyleColor() { - if (IsDarkStyle()){ + if (frameOptions.style == THEME_STYLE_FRAPPE){ + return QColor(0x30, 0x34, 0x46); // Catppuccin Frappe "Base" + } + else if (frameOptions.style == THEME_STYLE_LATTE){ + return QColor(0xef, 0xf1, 0xf5); // Catppuccin Latte "Base" + } + else if (IsDarkStyle()){ return QColor(38, 38, 38); } else{ @@ -463,11 +508,19 @@ QColor AppConfig::GetStyleColor() //-------------api QString GetIconPath() -{ +{ QString style = AppConfig::Instance().frameOptions.style; if (style == ""){ style = THEME_STYLE_DARK; } + // Latte/Frappe don't have their own icon sets - they reuse whichever of + // the light/dark icon sets already matches their background darkness. + if (AppConfig::Instance().IsDarkStyle()){ + style = THEME_STYLE_DARK; + } + else{ + style = THEME_STYLE_LIGHT; + } return ":/icons/" + style; } @@ -478,7 +531,7 @@ QString GetAppDataDir() QDir dir(QCoreApplication::applicationDirPath()); if (dir.cd("..") && dir.cd("share") && dir.cd("DSView")) { - return dir.absolutePath(); + return dir.absolutePath(); } QDir dir1("/usr/local/share/DSView"); if (dir1.exists()){ @@ -486,7 +539,7 @@ QString GetAppDataDir() } dsv_err("Data directory is not exists: ../share/DSView"); - assert(false); + assert(false); #else #ifdef Q_OS_DARWIN @@ -521,7 +574,7 @@ QString GetFirmwareDir() { return dir.absolutePath(); } - + dsv_err("%s%s", "Resource directory is not exists:", dir1.absolutePath().toUtf8().data()); return dir1.absolutePath(); } @@ -543,14 +596,14 @@ QString GetDecodeScriptDir() // ./decoders if (dir1.exists(path)) { - return path; QColor GetStyleColor(); + return path; } QDir dir(QCoreApplication::applicationDirPath()); // ../share/libsigrokdecode4DSL/decoders if (dir.cd("..") && dir.cd("share") && dir.cd("libsigrokdecode4DSL") && dir.cd("decoders")) { - return dir.absolutePath(); + return dir.absolutePath(); } dsv_info("ERROR: the decoder directory is not exists: ../share/libsigrokdecode4DSL/decoders"); return ""; diff --git a/DSView/pv/config/appconfig.h b/DSView/pv/config/appconfig.h index b4fa86819..faf3bec7f 100644 --- a/DSView/pv/config/appconfig.h +++ b/DSView/pv/config/appconfig.h @@ -29,9 +29,12 @@ #define LAN_CN 25 #define LAN_EN 31 +#define LAN_DE 7 #define THEME_STYLE_DARK "dark" #define THEME_STYLE_LIGHT "light" +#define THEME_STYLE_LATTE "latte" +#define THEME_STYLE_FRAPPE "frappe" #define APP_NAME "DSView" @@ -76,11 +79,16 @@ struct AppOptions bool autoScrollLatestData; bool verticalScrollIsZoom; float fontSize; + float traceHeightFactor; QString rulerTimeUnits; bool antialias; int minDecoderFontWidthPercent; int maxDecoderFontWidthPercent; bool decoderDynamicFontWidth; + bool dontAskSaveOnExit; + float logicSignalLineWidth; + bool logicChannelDivider; + bool dsoSplitChannels; std::vector m_protocolFormats; }; @@ -124,6 +132,7 @@ struct UserHistory QString openDir; QString protocolExportPath; QString exportFormat; + bool showDriverHint; }; struct FontParam @@ -167,6 +176,10 @@ class AppConfig static void GetFontSizeRange(float *minSize, float *maxSize); + // The configured application font size, clamped to the valid range. Used + // for trace-area text so it honours the user's font-size setting. + float GetTraceFontSize(); + bool IsDarkStyle(); QColor GetStyleColor(); diff --git a/DSView/pv/data/analogsnapshot.cpp b/DSView/pv/data/analogsnapshot.cpp index f9ccffb78..d9773abe4 100644 --- a/DSView/pv/data/analogsnapshot.cpp +++ b/DSView/pv/data/analogsnapshot.cpp @@ -23,11 +23,13 @@ #include #include #include +#include #include #include - + #include "analogsnapshot.h" #include "../dsvdef.h" +#include "../log.h" using namespace std; @@ -125,7 +127,16 @@ void AnalogSnapshot::first_payload(const sr_datafeed_analog &analog, uint64_t to } bool isOk = true; - uint64_t size = _total_sample_count * _channel_num * _unit_bytes + sizeof(uint64_t); + const uint64_t unit_stride = (uint64_t)_channel_num * (uint64_t)_unit_bytes; + + if (unit_stride != 0 && _total_sample_count > (UINT64_MAX - sizeof(uint64_t)) / unit_stride) { + dsv_err("AnalogSnapshot::first_payload, sample buffer size overflow."); + free_data(); + _memory_failed = true; + return; + } + + uint64_t size = _total_sample_count * unit_stride + sizeof(uint64_t); if (size != _capacity) { free_data(); diff --git a/DSView/pv/data/decode/annotationrestable.cpp b/DSView/pv/data/decode/annotationrestable.cpp index 6f2ec96ab..9c248edef 100644 --- a/DSView/pv/data/decode/annotationrestable.cpp +++ b/DSView/pv/data/decode/annotationrestable.cpp @@ -21,8 +21,9 @@ #include "annotationrestable.h" #include -#include +#include #include +#include #include "../../log.h" #include "../../dsvdef.h" @@ -292,7 +293,7 @@ const char* AnnotationResTable::format_numberic(const char *hex_str, int fmt) return hex_str; } - strncpy(all_wr, sub_str, sublen); + memcpy(all_wr, sub_str, sublen); all_wr += sublen; sub_wr = sub_buf; //reset write buffer } @@ -320,7 +321,7 @@ const char* AnnotationResTable::format_numberic(const char *hex_str, int fmt) return hex_str; } - strncpy(all_wr, sub_str, sublen); + memcpy(all_wr, sub_str, sublen); all_wr += sublen; } diff --git a/DSView/pv/data/dsoedgedetect.h b/DSView/pv/data/dsoedgedetect.h new file mode 100644 index 000000000..f18c9b080 --- /dev/null +++ b/DSView/pv/data/dsoedgedetect.h @@ -0,0 +1,91 @@ +/* + * This file is part of the DSView project. + * DSView is based on PulseView. + * + * Copyright (C) 2026 Schildkroet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef DSVIEW_PV_DATA_DSOEDGEDETECT_H +#define DSVIEW_PV_DATA_DSOEDGEDETECT_H + +#include +#include +#include + +namespace pv { +namespace data { + +// Sample indices of the rising/falling edges found by dso_detect_edges(). +struct DsoEdgeSet +{ + std::vector rising; + std::vector falling; +}; + +// Schmitt-trigger edge detection over a DSO sample range: tracks a high/low +// state and flips it once the signal clears the upper/lower threshold (5% of +// hysteresis around the value's midpoint), recording the sample index of each +// transition. This tolerates finite rise time and noise, unlike a single-step +// "crossed mid this sample" test. +// +// value_at(i) must return the signal's value for sample index i, in whatever +// unit is convenient at the call site (raw ADC counts, volts, hw_offset- +// relative counts, ...) - only relative comparisons are made, so the unit +// does not matter as long as it is used consistently across the whole range. +// Returns an empty set if there are fewer than 2 samples or the signal is +// flat (max <= min), since no threshold could then be established. +template +DsoEdgeSet dso_detect_edges(uint64_t sample_count, ValueFn &&value_at) +{ + DsoEdgeSet edges; + + if (sample_count < 2) + return edges; + + double vmin = 1e300, vmax = -1e300; + for (uint64_t i = 0; i < sample_count; i++) { + const double v = value_at(i); + vmin = std::min(vmin, v); + vmax = std::max(vmax, v); + } + if (vmax <= vmin) + return edges; // flat signal: no threshold to detect edges against + + const double mid = (vmin + vmax) / 2.0; + const double hyst = (vmax - vmin) * 0.05; + const double hi = mid + hyst; + const double lo = mid - hyst; + + bool is_high = (value_at(0) >= mid); + for (uint64_t i = 1; i < sample_count; i++) { + const double v = value_at(i); + if (!is_high && v >= hi) { + is_high = true; + edges.rising.push_back(i); + } else if (is_high && v <= lo) { + is_high = false; + edges.falling.push_back(i); + } + } + + return edges; +} + +} // namespace data +} // namespace pv + +#endif // DSVIEW_PV_DATA_DSOEDGEDETECT_H diff --git a/DSView/pv/data/dsosnapshot.cpp b/DSView/pv/data/dsosnapshot.cpp index 97167fde2..c9bc1b598 100644 --- a/DSView/pv/data/dsosnapshot.cpp +++ b/DSView/pv/data/dsosnapshot.cpp @@ -526,6 +526,7 @@ bool DsoSnapshot::get_max_min_value(uint8_t &maxv, uint8_t &minv, int chan_index if (chan_index < 0 || chan_index >= (int)_ch_data.size()){ assert(false); + return false; } uint8_t *p = _ch_data[chan_index]; diff --git a/DSView/pv/data/logicsnapshot.cpp b/DSView/pv/data/logicsnapshot.cpp index b2226cd8c..ce933930a 100644 --- a/DSView/pv/data/logicsnapshot.cpp +++ b/DSView/pv/data/logicsnapshot.cpp @@ -335,8 +335,9 @@ void LogicSnapshot::append_cross_payload(const sr_datafeed_logic &logic) if (index0 >= _ch_data[0].size()){ assert(false); + return; } - + lbp = _ch_data[fill_chan_index][index0].lbp[index1]; if (lbp == NULL){ lbp = malloc(LeafBlockSpace); @@ -1293,6 +1294,10 @@ bool LogicSnapshot::pattern_search_self(int64_t start, int64_t end, int64_t &ind int channel = it->first; if (flag != 'X' && has_data(channel)){ + if (count >= CHANNEL_MAX_COUNT){ + assert(false); + break; + } flagList[count] = flag; chanIndexs[count] = channel; count++; @@ -1489,6 +1494,13 @@ uint8_t *LogicSnapshot::get_block_buf_unlock(int block_index, int sig_index, boo uint64_t index = block_index / RootScale; uint8_t pos = block_index % RootScale; + + if (index >= _ch_data[order].size()){ + assert(false); + sample = 0; + return NULL; + } + uint8_t *lbp = (uint8_t*)_ch_data[order][index].lbp[pos]; if (lbp == NULL){ diff --git a/DSView/pv/data/mathstack.cpp b/DSView/pv/data/mathstack.cpp index 817768ad6..0f506d6bb 100644 --- a/DSView/pv/data/mathstack.cpp +++ b/DSView/pv/data/mathstack.cpp @@ -3,6 +3,7 @@ * DSView is based on PulseView. * * Copyright (C) 2016 DreamSourceLab + * Copyright (C) 2026 Schildkroet * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -86,6 +87,7 @@ MathStack::MathStack(pv::SigSession *session, _dsoSig1(dsoSig1), _dsoSig2(dsoSig2), _type(type), + _filter_width(10), _sample_num(0), _total_sample_num(0), _math_state(Init), @@ -181,6 +183,9 @@ uint64_t MathStack::default_vDialValue() case MATH_DIV: value = dial1_value * 1000.0 / dial2_value; break; + default: // unary operators: scale off the single source + value = dial1_value; + break; } bool bFind = false; @@ -221,6 +226,9 @@ uint64_t MathStack::default_factor() case MATH_DIV: value = factor1 / factor2; break; + default: // unary operators: single-source factor + value = factor1; + break; } if (value == 0){ @@ -276,6 +284,17 @@ view::dslDial* MathStack::get_vDial() for(int i = 0; i < vDialUnitCount; i++) vUnit.append(vDialDivUnit[i]); break; + default: // unary operators: derive the dial from the single source + for (int i = 0; i < vDialValueCount; i++) { + if (vDialValue[i] < dial1_min) + continue; + vValue.append(vDialValue[i]); + if (vDialValue[i] > dial1_max) + break; + } + for(int i = 0; i < vDialUnitCount; i++) + vUnit.append(vDialAddUnit[i]); + break; } view::dslDial *vDial = new view::dslDial(vValue.count(), vDialValueStep, vValue, vUnit, true); @@ -299,6 +318,9 @@ QString MathStack::get_unit(int level) case MATH_DIV: unit = vDialDivUnit[level]; break; + default: // unary operators use voltage-style units + unit = vDialAddUnit[level]; + break; } return unit; @@ -319,6 +341,9 @@ double MathStack::get_math_scale() case MATH_DIV: scale = 1.0 / DS_CONF_DSO_VDIVS; break; + default: // unary operators + scale = 1.0 / DS_CONF_DSO_VDIVS; + break; } return scale; @@ -371,52 +396,112 @@ void MathStack::calc_math(uint64_t mathFactor) if (data->empty() || _math.size() < _total_sample_num) return; - if (!_dsoSig1->enabled() || !_dsoSig2->enabled()) + const bool unary = is_unary(_type); + + if (!_dsoSig1->enabled()) return; - if (data->get_channel_num() < 2) + // The binary operators need a valid, enabled 2nd source; the unary ones + // (integrate/differentiate/abs/square/sqrt/filters) work off src1 alone. + if (!unary && (!_dsoSig2->enabled() || data->get_channel_num() < 2)) return; auto k1 = _dsoSig1->get_factor(); - auto k2 = _dsoSig2->get_factor(); const double scale1 = _dsoSig1->get_vDialValue() / 1000.0 * k1 * DS_CONF_DSO_VDIVS * _dsoSig1->get_scale() / _dsoSig1->get_view_rect().height(); const double delta1 = _dsoSig1->get_hw_offset() * scale1; - const double scale2 = _dsoSig2->get_vDialValue() / 1000.0 * k2 * DS_CONF_DSO_VDIVS * - _dsoSig2->get_scale() / _dsoSig2->get_view_rect().height(); - - const double delta2 = _dsoSig2->get_hw_offset() * scale2; - _sample_num = data->get_sample_count(); assert(_sample_num <= _total_sample_num); const int index1 = _dsoSig1->get_index(); - const int index2 = _dsoSig2->get_index(); const uint8_t* value_buffer1 = data->get_samples(0, 0, index1); - const uint8_t* value_buffer2 = data->get_samples(0, 0, index2); - double value1, value2; - for (uint64_t sample = 0; sample < _sample_num; sample++) { - value1 = *(value_buffer1 + sample); - value2 = *(value_buffer2 + sample); + // Source #1 in volts for a given sample index. + auto v1 = [&](uint64_t s) -> double { + return delta1 - scale1 * (*(value_buffer1 + s)); + }; - switch(_type) - { - case MATH_ADD: - _math[sample] = ((delta1 - scale1 * value1) + (delta2 - scale2 * value2)) / mathFactor; - break; - case MATH_SUB: - _math[sample] = ((delta1 - scale1 * value1) - (delta2 - scale2 * value2)) / mathFactor; - break; - case MATH_MUL: - _math[sample] = (delta1 - scale1 * value1) * (delta2 - scale2 * value2) / mathFactor; - break; - case MATH_DIV: - _math[sample] = (delta1 - scale1 * value1) / (delta2 - scale2 * value2) / mathFactor; - break; + if (unary) { + const double dt = (samplerate() > 0) ? 1.0 / samplerate() : 1.0; + const uint64_t win = (uint64_t)max(1, _filter_width); + double integ = 0.0; + double acc = 0.0; // running window sum for the filters + + for (uint64_t sample = 0; sample < _sample_num; sample++) { + const double v = v1(sample); + double r = 0.0; + + switch(_type) + { + case MATH_INTEG: + integ += v * dt; + r = integ; + break; + case MATH_DIFF: + r = (v - v1(sample == 0 ? 0 : sample - 1)) / dt; + break; + case MATH_ABS: + r = fabs(v); + break; + case MATH_SQUARE: + r = v * v; + break; + case MATH_SQRT: + r = (v < 0) ? -sqrt(-v) : sqrt(v); + break; + case MATH_LOWPASS: + case MATH_HIGHPASS: { + acc += v; + if (sample >= win) + acc -= v1(sample - win); + const uint64_t n = min(sample + 1, win); + const double avg = acc / n; + r = (_type == MATH_LOWPASS) ? avg : (v - avg); + break; + } + default: + r = v; + break; + } + + _math[sample] = r / mathFactor; + } + } + else { + auto k2 = _dsoSig2->get_factor(); + + const double scale2 = _dsoSig2->get_vDialValue() / 1000.0 * k2 * DS_CONF_DSO_VDIVS * + _dsoSig2->get_scale() / _dsoSig2->get_view_rect().height(); + + const double delta2 = _dsoSig2->get_hw_offset() * scale2; + + const int index2 = _dsoSig2->get_index(); + const uint8_t* value_buffer2 = data->get_samples(0, 0, index2); + + for (uint64_t sample = 0; sample < _sample_num; sample++) { + const double value1 = v1(sample); + const double value2 = delta2 - scale2 * (*(value_buffer2 + sample)); + + switch(_type) + { + case MATH_ADD: + _math[sample] = (value1 + value2) / mathFactor; + break; + case MATH_SUB: + _math[sample] = (value1 - value2) / mathFactor; + break; + case MATH_MUL: + _math[sample] = value1 * value2 / mathFactor; + break; + case MATH_DIV: + _math[sample] = value1 / value2 / mathFactor; + break; + default: + break; + } } } diff --git a/DSView/pv/data/mathstack.h b/DSView/pv/data/mathstack.h index 216d4119b..dfbf5ba78 100644 --- a/DSView/pv/data/mathstack.h +++ b/DSView/pv/data/mathstack.h @@ -3,6 +3,7 @@ * DSView is based on PulseView. * * Copyright (C) 2016 DreamSourceLab + * Copyright (C) 2026 Schildkroet * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -61,8 +62,21 @@ class MathStack : public QObject, public SignalData MATH_SUB, MATH_MUL, MATH_DIV, + // Unary operators below only use the 1st source. + MATH_INTEG, // running integral (∫ x dt) + MATH_DIFF, // time derivative (d/dt) + MATH_ABS, // absolute value (|x|) + MATH_SQUARE, // square (x²) + MATH_SQRT, // signed sqrt (√x) + MATH_LOWPASS, // moving-average low-pass + MATH_HIGHPASS, // x - moving-average (high-pass) }; + // True for the single-source operators (everything from MATH_INTEG on). + static bool is_unary(MathType type) { + return type >= MATH_INTEG; + } + struct EnvelopeSample { double min; @@ -113,6 +127,10 @@ class MathStack : public QObject, public SignalData MathType get_type(); uint64_t get_sample_num(); + // Window length (in samples) for the moving-average LP/HP filters. + void set_filter_width(int width) { _filter_width = (width < 1) ? 1 : width; } + int get_filter_width() { return _filter_width; } + void enable_envelope(bool enable); uint64_t default_vDialValue(); @@ -138,6 +156,7 @@ class MathStack : public QObject, public SignalData view::DsoSignal *_dsoSig2; MathType _type; + int _filter_width; uint64_t _sample_num; uint64_t _total_sample_num; math_state _math_state; diff --git a/DSView/pv/deviceagent.cpp b/DSView/pv/deviceagent.cpp index 6db0e58b5..b4f9b3a11 100644 --- a/DSView/pv/deviceagent.cpp +++ b/DSView/pv/deviceagent.cpp @@ -364,7 +364,8 @@ GVariant* DeviceAgent::get_config_list(const sr_channel_group *group, int key) dsv_detail("%s%d", "WARNING: Failed to get config list, key:", key); if (data != NULL){ - dsv_warn("%s%d", "WARNING: Failed to get config list, but data is not null. key:", key); + dsv_warn("%s%d", "WARNING: Failed to get config list, but data is not null. key:", key); + g_variant_unref(data); } data = NULL; } diff --git a/DSView/pv/dialogs/applicationpardlg.cpp b/DSView/pv/dialogs/applicationpardlg.cpp index b12fbf502..d900ad425 100644 --- a/DSView/pv/dialogs/applicationpardlg.cpp +++ b/DSView/pv/dialogs/applicationpardlg.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -138,6 +139,9 @@ bool ApplicationParamDlg::ShowDlg(QWidget *parent) QCheckBox *ck_autoScrollLatestData = new QCheckBox(); ck_autoScrollLatestData->setChecked(app.appOptions.autoScrollLatestData); + QCheckBox *ck_channelDivider = new QCheckBox(); + ck_channelDivider->setChecked(app.appOptions.logicChannelDivider); + QHBoxLayout *hl_verticalScrollAction = new QHBoxLayout(); QButtonGroup *bg_verticalScrollAction = new QButtonGroup(); QRadioButton *rb_zoom = new QRadioButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_VERTICAL_SCROLL_ACTION_SMOOTH_ZOOM), "Zoom")); @@ -151,6 +155,9 @@ bool ApplicationParamDlg::ShowDlg(QWidget *parent) QCheckBox *ck_antialias = new QCheckBox(); ck_antialias->setChecked(app.appOptions.antialias); + QCheckBox *ck_dontAskSaveOnExit = new QCheckBox(); + ck_dontAskSaveOnExit->setChecked(app.appOptions.dontAskSaveOnExit); + QComboBox *ftCbSize = new DsComboBox(); ftCbSize->setFixedWidth(50); bind_font_size_list(ftCbSize, app.appOptions.fontSize); @@ -173,6 +180,14 @@ bool ApplicationParamDlg::ShowDlg(QWidget *parent) QCheckBox *ck_decoderDynamicFontWidth = new QCheckBox(); ck_decoderDynamicFontWidth->setChecked(fontWidthEnabled); + QDoubleSpinBox *spinBox_lineWidth = new QDoubleSpinBox(); + spinBox_lineWidth->setDecimals(1); + spinBox_lineWidth->setSingleStep(0.5); + spinBox_lineWidth->setMinimum(1.0); + spinBox_lineWidth->setMaximum(4.0); + spinBox_lineWidth->setValue(app.appOptions.logicSignalLineWidth); + spinBox_lineWidth->setSuffix(" px"); + // Logic group QGroupBox *logicGroup = new QGroupBox(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_GROUP_LOGIC), "Logic")); QGridLayout *logicLay = new QGridLayout(); @@ -288,18 +303,22 @@ bool ApplicationParamDlg::ShowDlg(QWidget *parent) logicLay->addLayout(hl_verticalScrollAction, 3, 1, Qt::AlignRight); logicLay->addWidget(new QLabel(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_RULER_UNITS), "Ruler / Cursor units")), 4, 0, Qt::AlignLeft); logicLay->addLayout(hl_units, 4, 1, Qt::AlignRight); + logicLay->addWidget(new QLabel(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_SIGNAL_LINE_WIDTH), "Signal line width")), 5, 0, Qt::AlignLeft); + logicLay->addWidget(spinBox_lineWidth, 5, 1, Qt::AlignRight); + logicLay->addWidget(new QLabel(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_CHANNEL_DIVIDER), "Channel divider line")), 6, 0, Qt::AlignLeft); + logicLay->addWidget(ck_channelDivider, 6, 1, Qt::AlignRight); // Add sliders to logic layout - logicLay->addWidget(new QLabel(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_DECODER_DYNAMIC_FONT_WIDTH), "Decoder adaptive font width")), 5, 0, Qt::AlignLeft); - logicLay->addWidget(ck_decoderDynamicFontWidth, 5, 1, Qt::AlignRight); - logicLay->addWidget(label_minFontWidth, 6, 0, Qt::AlignLeft); - logicLay->addWidget(spinBox_minFontWidth, 6, 1, Qt::AlignRight); - logicLay->addWidget(slider_minFontWidth, 7, 0, Qt::AlignJustify); - logicLay->addWidget(label_minSample, 7, 1, Qt::AlignCenter); - logicLay->addWidget(label_maxFontWidth, 8, 0, Qt::AlignLeft); - logicLay->addWidget(spinBox_maxFontWidth, 8, 1, Qt::AlignRight); - logicLay->addWidget(slider_maxFontWidth, 9, 0, Qt::AlignJustify); - logicLay->addWidget(label_maxSample, 9, 1, Qt::AlignCenter); + logicLay->addWidget(new QLabel(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_DECODER_DYNAMIC_FONT_WIDTH), "Decoder adaptive font width")), 7, 0, Qt::AlignLeft); + logicLay->addWidget(ck_decoderDynamicFontWidth, 7, 1, Qt::AlignRight); + logicLay->addWidget(label_minFontWidth, 8, 0, Qt::AlignLeft); + logicLay->addWidget(spinBox_minFontWidth, 8, 1, Qt::AlignRight); + logicLay->addWidget(slider_minFontWidth, 9, 0, Qt::AlignJustify); + logicLay->addWidget(label_minSample, 9, 1, Qt::AlignCenter); + logicLay->addWidget(label_maxFontWidth, 10, 0, Qt::AlignLeft); + logicLay->addWidget(spinBox_maxFontWidth, 10, 1, Qt::AlignRight); + logicLay->addWidget(slider_maxFontWidth, 11, 0, Qt::AlignJustify); + logicLay->addWidget(label_maxSample, 11, 1, Qt::AlignCenter); logicLay->setColumnMinimumWidth(1, wfm.horizontalAdvance("Example") + logicLay->contentsMargins().left() + logicLay->contentsMargins().right() @@ -328,6 +347,8 @@ bool ApplicationParamDlg::ShowDlg(QWidget *parent) uiLay->addWidget(ftCbSize, 1, 1, Qt::AlignRight); uiLay->addWidget(new QLabel(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_DISPLAY_ANTIALIAS), "Antialiasing")), 2, 0, Qt::AlignLeft); uiLay->addWidget(ck_antialias, 2, 1, Qt::AlignRight); + uiLay->addWidget(new QLabel(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_DONT_ASK_SAVE_ON_EXIT), "Do not ask to save captured data")), 3, 0, Qt::AlignLeft); + uiLay->addWidget(ck_dontAskSaveOnExit, 3, 1, Qt::AlignRight); lay->addWidget(uiGroup); dlg.layout()->addLayout(lay); @@ -381,6 +402,10 @@ bool ApplicationParamDlg::ShowDlg(QWidget *parent) app.appOptions.antialias = ck_antialias->isChecked(); bAppChanged = true; } + if (app.appOptions.dontAskSaveOnExit != ck_dontAskSaveOnExit->isChecked()){ + app.appOptions.dontAskSaveOnExit = ck_dontAskSaveOnExit->isChecked(); + bAppChanged = true; + } if (app.appOptions.decoderDynamicFontWidth != ck_decoderDynamicFontWidth->isChecked()) { app.appOptions.decoderDynamicFontWidth = ck_decoderDynamicFontWidth->isChecked(); bAppChanged = true; @@ -393,6 +418,14 @@ bool ApplicationParamDlg::ShowDlg(QWidget *parent) app.appOptions.maxDecoderFontWidthPercent = slider_maxFontWidth->value(); bAppChanged = true; } + if (app.appOptions.logicSignalLineWidth != spinBox_lineWidth->value()) { + app.appOptions.logicSignalLineWidth = spinBox_lineWidth->value(); + bAppChanged = true; + } + if (app.appOptions.logicChannelDivider != ck_channelDivider->isChecked()) { + app.appOptions.logicChannelDivider = ck_channelDivider->isChecked(); + bAppChanged = true; + } if (bAppChanged){ app.SaveApp(); AppControl::Instance()->GetSession()->broadcast_msg(DSV_MSG_APP_OPTIONS_CHANGED); diff --git a/DSView/pv/dialogs/calibration.cpp b/DSView/pv/dialogs/calibration.cpp index 9ddd066cb..be5e60be9 100644 --- a/DSView/pv/dialogs/calibration.cpp +++ b/DSView/pv/dialogs/calibration.cpp @@ -201,7 +201,7 @@ void Calibration::update_device_info() for (const GSList *l = _device_agent->get_channels(); l; l = l->next) { sr_channel *const probe = (sr_channel*)l->data; dex++; - assert(dex < _params.size()); + assert((size_t)dex < _params.size()); auto *form = &_params[dex]; assert(form->probe == probe); diff --git a/DSView/pv/dialogs/chanmeasure.cpp b/DSView/pv/dialogs/chanmeasure.cpp new file mode 100644 index 000000000..90ddc5aa3 --- /dev/null +++ b/DSView/pv/dialogs/chanmeasure.cpp @@ -0,0 +1,327 @@ +/* + * This file is part of the DSView project. + * DSView is based on PulseView. + * + * Copyright (C) 2026 Schildkroet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "chanmeasure.h" + +#include +#include +#include + +#include +#include + +#include "../sigsession.h" +#include "../view/dsosignal.h" +#include "../data/dsosnapshot.h" +#include "../data/dsoedgedetect.h" +#include "../ui/langresource.h" + +using namespace std; + +namespace pv { +namespace dialogs { + +namespace { + +// Threshold-crossing edges of one DSO channel, in nanoseconds, plus the mean +// period. Uses a mid-level threshold with 5% hysteresis to reject noise. +struct EdgeInfo +{ + bool valid = false; + std::vector rising; // rising-edge times (ns) + std::vector falling; // falling-edge times (ns) + double period = 0.0; // ns (mean rising-to-rising) +}; + +EdgeInfo extract_edges(view::DsoSignal *sig, double dt_ns) +{ + EdgeInfo info; + + if (sig == NULL || !sig->enabled() || dt_ns <= 0) + return info; + + data::DsoSnapshot *data = sig->data(); + if (data == NULL || data->empty()) + return info; + + const uint64_t total = data->get_sample_count(); + const uint8_t *buf = data->get_samples(0, 0, sig->get_index()); + if (total < 2 || buf == NULL) + return info; + + const int hw_offset = sig->get_hw_offset(); + const double k = data->get_measure_voltage_factor(sig->get_index()); + const double data_scale = data->get_data_scale(sig->get_index()); + const double vfactor = sig->get_vDial()->get_factor(); + const int vrect_h = sig->get_view_rect().height(); + const double vscale = (vrect_h > 0) + ? data_scale * k * vfactor * DS_CONF_DSO_VDIVS / vrect_h : 0.0; + + const uint64_t MaxSamples = 8000000; + const uint64_t n = min(total, MaxSamples); + + auto volt = [&](uint64_t i) -> double { + return (hw_offset - (double)buf[i]) * vscale; + }; + + // Fully qualified: the local variable "data" above shadows the "data" + // namespace within this function. + const pv::data::DsoEdgeSet edge_set = pv::data::dso_detect_edges(n, volt); + + // Convert sample indices to times (ns) for this dialog's phase/delay math. + info.rising.reserve(edge_set.rising.size()); + for (uint64_t idx : edge_set.rising) + info.rising.push_back(idx * dt_ns); + info.falling.reserve(edge_set.falling.size()); + for (uint64_t idx : edge_set.falling) + info.falling.push_back(idx * dt_ns); + + if (info.rising.size() >= 2) { + double sum = 0; + for (size_t e = 1; e < info.rising.size(); e++) + sum += info.rising[e] - info.rising[e - 1]; + info.period = sum / (info.rising.size() - 1); + } + + info.valid = true; + return info; +} + +// Mean signed delay (ns) from A's edges to B's edges, wrapped into +// (-period/2, +period/2] so it reads as the fractional shift within a cycle. +// Returns false if it cannot be computed. +bool mean_delay(const std::vector &ea, const std::vector &eb, + double period, double &out_ns) +{ + if (ea.empty() || eb.empty() || period <= 0) + return false; + + double sum = 0; + int cnt = 0; + for (double ta : ea) { + // Nearest B edge to ta. + auto it = std::lower_bound(eb.begin(), eb.end(), ta); + double best = 0; + bool have = false; + if (it != eb.end()) { + best = *it - ta; + have = true; + } + if (it != eb.begin()) { + const double d = *(it - 1) - ta; + if (!have || fabs(d) < fabs(best)) { + best = d; + have = true; + } + } + if (!have) + continue; + + // Wrap into (-period/2, +period/2]. + while (best > period / 2) best -= period; + while (best <= -period / 2) best += period; + + sum += best; + cnt++; + } + + if (cnt == 0) + return false; + + out_ns = sum / cnt; + return true; +} + +QString fmt_time_ns(double t_ns) +{ + const double a = fabs(t_ns); + if (a >= 1e9) + return QString::number(t_ns / 1e9, 'f', 3) + " s"; + if (a >= 1e6) + return QString::number(t_ns / 1e6, 'f', 3) + " ms"; + if (a >= 1e3) + return QString::number(t_ns / 1e3, 'f', 3) + " us"; + return QString::number(t_ns, 'f', 2) + " ns"; +} + +QString fmt_freq_hz(double f_hz) +{ + const double a = fabs(f_hz); + if (a >= 1e6) + return QString::number(f_hz / 1e6, 'f', 3) + " MHz"; + if (a >= 1e3) + return QString::number(f_hz / 1e3, 'f', 3) + " kHz"; + return QString::number(f_hz, 'f', 2) + " Hz"; +} + +} // namespace + +DsoChannelMeasure::DsoChannelMeasure(SigSession *session, QWidget *parent) : + DSDialog(parent), + _session(session), + _button_box(QDialogButtonBox::Close, Qt::Horizontal, this) +{ + setMinimumSize(380, 260); + + _srcA_combobox = new DsComboBox(this); + _srcB_combobox = new DsComboBox(this); + for (auto s : _session->get_signals()) { + if (s->signal_type() == SR_CHANNEL_DSO) { + view::DsoSignal *dsoSig = (view::DsoSignal*)s; + _srcA_combobox->addItem(dsoSig->get_name(), + QVariant::fromValue(dsoSig->get_index())); + _srcB_combobox->addItem(dsoSig->get_name(), + QVariant::fromValue(dsoSig->get_index())); + } + } + // Default the 2nd source to a different channel when possible. + if (_srcB_combobox->count() > 1) + _srcB_combobox->setCurrentIndex(1); + + _result_label = new QLabel(this); + _result_label->setTextInteractionFlags(Qt::TextSelectableByMouse); + _result_label->setWordWrap(true); + _result_label->setAlignment(Qt::AlignTop | Qt::AlignLeft); + + QFormLayout *src_layout = new QFormLayout(); + src_layout->addRow( + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_1ST_SOURCE), "1st Source"), _srcA_combobox); + src_layout->addRow( + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_2ST_SOURCE), "2st Source"), _srcB_combobox); + + _layout = new QVBoxLayout(); + _layout->addLayout(src_layout); + _layout->addWidget(_result_label, 1); + _layout->addWidget(&_button_box); + + layout()->addLayout(_layout); + setTitle(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_CH_MEASURE), + "Channel-to-channel measure")); + + connect(&_button_box, SIGNAL(rejected()), this, SLOT(reject())); + connect(&_button_box, SIGNAL(accepted()), this, SLOT(accept())); + connect(_srcA_combobox, SIGNAL(currentIndexChanged(int)), + this, SLOT(on_source_changed(int))); + connect(_srcB_combobox, SIGNAL(currentIndexChanged(int)), + this, SLOT(on_source_changed(int))); + + compute(); +} + +DsoChannelMeasure::~DsoChannelMeasure() +{ +} + +void DsoChannelMeasure::on_source_changed(int index) +{ + (void)index; + compute(); +} + +void DsoChannelMeasure::compute() +{ + const int idxA = (_srcA_combobox->count() > 0) + ? _srcA_combobox->itemData(_srcA_combobox->currentIndex()).toInt() : -1; + const int idxB = (_srcB_combobox->count() > 0) + ? _srcB_combobox->itemData(_srcB_combobox->currentIndex()).toInt() : -1; + + view::DsoSignal *sigA = NULL; + view::DsoSignal *sigB = NULL; + for (auto s : _session->get_signals()) { + if (s->signal_type() == SR_CHANNEL_DSO) { + view::DsoSignal *d = (view::DsoSignal*)s; + if (d->get_index() == idxA) sigA = d; + if (d->get_index() == idxB) sigB = d; + } + } + + if (sigA == NULL || sigB == NULL) { + _result_label->setText( + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_CH_NEED_TWO), + "Select two DSO channels.")); + return; + } + if (sigA == sigB) { + _result_label->setText( + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_CH_NEED_DIFF), + "Select two different channels.")); + return; + } + + // Prefer the analysed snapshot's own sample rate (matches the waveform + // painter); fall back to the session's capture-buffer rate. + double samplerate = (sigA->data() != NULL) ? sigA->data()->samplerate() : 0.0; + if (samplerate <= 0) + samplerate = _session->cur_snap_samplerate(); + const double dt_ns = (samplerate > 0) ? 1e9 / samplerate : 0.0; + + EdgeInfo ea = extract_edges(sigA, dt_ns); + EdgeInfo eb = extract_edges(sigB, dt_ns); + + if (!ea.valid || !eb.valid) { + _result_label->setText( + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_HIST_NO_DATA), + "No captured data. Run an acquisition first.")); + return; + } + + QString text; + const QString none = "--"; + + const QString freqA = (ea.period > 0) + ? fmt_freq_hz(1e9 / ea.period) : none; + const QString freqB = (eb.period > 0) + ? fmt_freq_hz(1e9 / eb.period) : none; + + text += QString("Freq %1: %2\n").arg(sigA->get_name()).arg(freqA); + text += QString("Freq %1: %2\n\n").arg(sigB->get_name()).arg(freqB); + + // Reference period for phase: the 1st source's period. + const double period = ea.period; + + double delay_ns = 0, skew_ns = 0; + const bool have_delay = mean_delay(ea.rising, eb.rising, period, delay_ns); + const bool have_skew = mean_delay(ea.falling, eb.falling, period, skew_ns); + + text += QString("%1: %2\n") + .arg(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_CH_DELAY), "Delay (rise)")) + .arg(have_delay ? fmt_time_ns(delay_ns) : none); + text += QString("%1: %2\n") + .arg(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_CH_SKEW), "Skew (fall)")) + .arg(have_skew ? fmt_time_ns(skew_ns) : none); + + QString phase = none; + if (have_delay && period > 0) { + double deg = delay_ns / period * 360.0; + // Present in (-180, 180]. + while (deg > 180) deg -= 360; + while (deg <= -180) deg += 360; + phase = QString::number(deg, 'f', 2) + " °"; + } + text += QString("%1: %2") + .arg(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_CH_PHASE), "Phase")) + .arg(phase); + + _result_label->setText(text); +} + +} // namespace dialogs +} // namespace pv diff --git a/DSView/pv/dialogs/chanmeasure.h b/DSView/pv/dialogs/chanmeasure.h new file mode 100644 index 000000000..a9b9af950 --- /dev/null +++ b/DSView/pv/dialogs/chanmeasure.h @@ -0,0 +1,67 @@ +/* + * This file is part of the DSView project. + * DSView is based on PulseView. + * + * Copyright (C) 2026 Schildkroet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef DSVIEW_PV_CHANMEASURE_H +#define DSVIEW_PV_CHANMEASURE_H + +#include +#include +#include + +#include "dsdialog.h" +#include "../ui/dscombobox.h" + +namespace pv { + +class SigSession; + +namespace dialogs { + +// Channel-to-channel timing measurements (phase, delay, skew) between two DSO +// channels, computed from the current snapshot. +class DsoChannelMeasure : public DSDialog +{ + Q_OBJECT + +public: + DsoChannelMeasure(SigSession *session, QWidget *parent); + ~DsoChannelMeasure(); + +private slots: + void on_source_changed(int index); + +private: + void compute(); + +private: + SigSession *_session; + + DsComboBox *_srcA_combobox; + DsComboBox *_srcB_combobox; + QLabel *_result_label; + QVBoxLayout *_layout; + QDialogButtonBox _button_box; +}; + +} // namespace dialogs +} // namespace pv + +#endif // DSVIEW_PV_CHANMEASURE_H diff --git a/DSView/pv/dialogs/decoderoptionsdlg.cpp b/DSView/pv/dialogs/decoderoptionsdlg.cpp index bf0b8e933..6765f3ba6 100644 --- a/DSView/pv/dialogs/decoderoptionsdlg.cpp +++ b/DSView/pv/dialogs/decoderoptionsdlg.cpp @@ -212,10 +212,19 @@ void DecoderOptionsDlg::load_options_view() int real_content_width = _content_width; int content_height = _contentHeight; - // scroll + // scroll QSize tsize = dlg->sizeHint(); - int w = tsize.width(); - int other_height = 190 + h_ex2; + int w = tsize.width(); + int other_height = 190 + h_ex2; + + // The 190 constant was tuned for a small font; with larger fonts the + // non-scroll widgets (cursor combos, buttons, ...) grow taller. Derive the + // reserved height from the dialog's actual size hint so the scroll area is + // not over-sized, which would otherwise push the bottom widgets off-dialog. + int dynamic_other = tsize.height() - _contentHeight + 10; + if (dynamic_other > other_height) + other_height = dynamic_other; + content_height += 20; int cursor_line_width = lb1->sizeHint().width() + _start_comboBox->sizeHint().width(); diff --git a/DSView/pv/dialogs/deviceoptions.cpp b/DSView/pv/dialogs/deviceoptions.cpp index baa7198b5..269663aad 100644 --- a/DSView/pv/dialogs/deviceoptions.cpp +++ b/DSView/pv/dialogs/deviceoptions.cpp @@ -172,7 +172,11 @@ DeviceOptions::DeviceOptions(QWidget *parent) : } DeviceOptions::~DeviceOptions() -{ +{ + for(auto p : _probe_options_binding_list) { + delete p; + } + _probe_options_binding_list.clear(); } void DeviceOptions::ChannelChecked(int index, QObject *object) @@ -682,6 +686,9 @@ void DeviceOptions::analog_probes(QGridLayout &layout) using namespace Qt; _probes_checkBox_list.clear(); + for(auto p : _probe_options_binding_list) { + delete p; + } _probe_options_binding_list.clear(); _dso_channel_list.clear(); @@ -705,7 +712,7 @@ void DeviceOptions::analog_probes(QGridLayout &layout) probe_widget->setLayout(probe_layout); bool ch_enabled = probe->enabled; - if (ch_dex < _lst_probe_enabled_status.size()){ + if ((size_t)ch_dex < _lst_probe_enabled_status.size()){ ch_enabled = _lst_probe_enabled_status[ch_dex]; } diff --git a/DSView/pv/dialogs/dsohistogram.cpp b/DSView/pv/dialogs/dsohistogram.cpp new file mode 100644 index 000000000..451ccdfa8 --- /dev/null +++ b/DSView/pv/dialogs/dsohistogram.cpp @@ -0,0 +1,479 @@ +/* + * This file is part of the DSView project. + * DSView is based on PulseView. + * + * Copyright (C) 2026 Schildkroet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "dsohistogram.h" + +#include +#include +#include + +#include +#include + +#include "../sigsession.h" +#include "../view/dsosignal.h" +#include "../data/dsosnapshot.h" +#include "../data/dsoedgedetect.h" +#include "../ui/langresource.h" + +using namespace std; + +namespace pv { +namespace dialogs { + +//------------------------------------------------------------------- HistogramPlot + +HistogramPlot::HistogramPlot(QWidget *parent) : + QWidget(parent), + _x_min(0), + _x_max(0), + _has_mean(false), + _mean_value(0.0) +{ + setMinimumHeight(150); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); +} + +void HistogramPlot::set_data(const QVector &bins, double x_min, + double x_max, const QString &x_unit, + const QString &title, bool has_mean, + double mean_value) +{ + _bins = bins; + _x_min = x_min; + _x_max = x_max; + _x_unit = x_unit; + _title = title; + _has_mean = has_mean; + _mean_value = mean_value; + update(); +} + +void HistogramPlot::clear_data() +{ + _bins.clear(); + update(); +} + +QString HistogramPlot::fmt_value(double v) const +{ + return QString::number(v, 'f', 2) + " " + _x_unit; +} + +void HistogramPlot::paintEvent(QPaintEvent *event) +{ + (void)event; + + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, false); + + QColor fore(palette().color(foregroundRole())); + QColor bar = fore; + bar.setAlpha(160); + + // Size the title and axis-label bands to the actual font so they are never + // clipped (the UI font can be larger, e.g. in the German translation). + const int text_h = p.fontMetrics().height(); + + const int left = 4; + const int right = width() - 4; + const int top = text_h + 4; // room for the title + const int bottom = height() - text_h - 4; // room for the axis labels + + // Title. + p.setPen(fore); + p.drawText(QRect(left, 0, right - left, top), + Qt::AlignLeft | Qt::AlignVCenter, _title); + + if (bottom - top < 4) + return; + + // Plot frame. + p.setPen(QPen(fore, 1)); + p.drawRect(QRect(left, top, right - left, bottom - top)); + + if (_bins.isEmpty()) { + p.drawText(QRect(left, top, right - left, bottom - top), + Qt::AlignCenter, "--"); + return; + } + + double peak = 0; + for (double v : _bins) + peak = max(peak, v); + if (peak <= 0) + peak = 1; + + const int plot_w = right - left; + const int plot_h = bottom - top; + const int n = _bins.size(); + + // Gridlines (drawn under the bars): 3 horizontal divisions (25/50/75% of + // the peak count) and vertical divisions at the same 25/50/75% x + // fractions, so a bar's height and position can be read off against a + // scale instead of guessed against bare min/max endpoints. + QColor grid = fore; + grid.setAlpha(45); + p.setPen(QPen(grid, 1, Qt::DotLine)); + for (int k = 1; k <= 3; k++) { + const int gy = bottom - (int)(plot_h * (k / 4.0)); + p.drawLine(left + 1, gy, right - 1, gy); + const int gx = left + (int)(plot_w * (k / 4.0)); + p.drawLine(gx, top + 1, gx, bottom - 1); + } + + p.setPen(Qt::NoPen); + p.setBrush(bar); + for (int i = 0; i < n; i++) { + const int x0 = left + (int)((double)i / n * plot_w); + const int x1 = left + (int)((double)(i + 1) / n * plot_w); + const int h = (int)(_bins[i] / peak * (plot_h - 1)); + if (h > 0) + p.fillRect(QRect(x0, bottom - h, max(1, x1 - x0 - 1), h), bar); + } + + // Mean marker: a dashed vertical line plus a small label, so it is easy + // to see at a glance how the distribution sits relative to its average. + if (_has_mean && _x_max > _x_min) { + const double rate = (_mean_value - _x_min) / (_x_max - _x_min); + if (rate >= 0.0 && rate <= 1.0) { + QColor accent = palette().color(QPalette::Highlight); + const int mx = left + (int)(rate * plot_w); + p.setPen(QPen(accent, 1, Qt::DashLine)); + p.drawLine(mx, top + 1, mx, bottom - 1); + + p.setPen(accent); + const QString mean_str = "Mean " + fmt_value(_mean_value); + const int label_w = p.fontMetrics().horizontalAdvance(mean_str); + // Keep the label inside the frame regardless of which side the + // marker falls on. + int label_x = mx + 3; + if (label_x + label_w > right) + label_x = mx - 3 - label_w; + p.drawText(QRect(label_x, top + 2, label_w, text_h), mean_str); + } + } + + // Axis annotations, in the band below the frame: min/mid/max values + // matching the vertical gridlines above, plus the peak bin count inside + // the frame's top-right corner so bar heights read against a real scale + // rather than only relative to each other. + p.setPen(fore); + p.drawText(QRect(left, bottom, plot_w, height() - bottom), + Qt::AlignLeft | Qt::AlignVCenter, fmt_value(_x_min)); + p.drawText(QRect(left, bottom, plot_w, height() - bottom), + Qt::AlignHCenter | Qt::AlignVCenter, + fmt_value((_x_min + _x_max) / 2.0)); + p.drawText(QRect(left, bottom, plot_w, height() - bottom), + Qt::AlignRight | Qt::AlignVCenter, fmt_value(_x_max)); + + QColor peak_fore = fore; + peak_fore.setAlpha(180); + p.setPen(peak_fore); + p.drawText(QRect(left, top + 2, plot_w - 4, text_h), + Qt::AlignRight | Qt::AlignVCenter, + "n=" + QString::number((qulonglong)peak)); +} + +//------------------------------------------------------------------- DsoHistogram + +DsoHistogram::DsoHistogram(SigSession *session, QWidget *parent) : + DSDialog(parent), + _session(session), + _button_box(QDialogButtonBox::Close, Qt::Horizontal, this) +{ + setMinimumSize(552, 460); + + _ch_combobox = new DsComboBox(this); + for (auto s : _session->get_signals()) { + if (s->signal_type() == SR_CHANNEL_DSO) { + view::DsoSignal *dsoSig = (view::DsoSignal*)s; + _ch_combobox->addItem(dsoSig->get_name(), + QVariant::fromValue(dsoSig->get_index())); + } + } + + _value_plot = new HistogramPlot(this); + _time_plot = new HistogramPlot(this); + + _stats_label = new QLabel(this); + _stats_label->setTextInteractionFlags(Qt::TextSelectableByMouse); + _stats_label->setWordWrap(true); + _stats_label->setTextFormat(Qt::RichText); + + // Voltage unit selector (values are computed in mV internally). + _unit_combobox = new DsComboBox(this); + _unit_combobox->addItem("mV", QVariant::fromValue(1.0)); + _unit_combobox->addItem("V", QVariant::fromValue(0.001)); + // DsComboBox forces AdjustToContents, which makes setMinimumWidth() a + // no-op; size it by a minimum contents length instead so the item text + // (plus the drop-down arrow) is not cropped. + _unit_combobox->setSizeAdjustPolicy( + QComboBox::AdjustToMinimumContentsLengthWithIcon); + _unit_combobox->setMinimumContentsLength(5); + + QLabel *ch_label = new QLabel( + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_CHANNEL), "Channel"), this); + QLabel *unit_label = new QLabel( + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_UNIT), "Unit"), this); + QHBoxLayout *ch_layout = new QHBoxLayout(); + ch_layout->addWidget(ch_label); + ch_layout->addWidget(_ch_combobox); + ch_layout->addStretch(1); + ch_layout->addWidget(unit_label); + ch_layout->addWidget(_unit_combobox); + + _layout = new QVBoxLayout(); + _layout->addLayout(ch_layout); + _layout->addWidget(_value_plot, 1); + _layout->addWidget(_time_plot, 1); + _layout->addWidget(_stats_label); + _layout->addWidget(&_button_box); + + layout()->addLayout(_layout); + setTitle(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_HISTOGRAM), "Histogram / Jitter")); + + connect(&_button_box, SIGNAL(rejected()), this, SLOT(reject())); + connect(&_button_box, SIGNAL(accepted()), this, SLOT(accept())); + connect(_ch_combobox, SIGNAL(currentIndexChanged(int)), + this, SLOT(on_channel_changed(int))); + connect(_unit_combobox, SIGNAL(currentIndexChanged(int)), + this, SLOT(on_channel_changed(int))); + + compute(); +} + +DsoHistogram::~DsoHistogram() +{ +} + +void DsoHistogram::on_channel_changed(int index) +{ + (void)index; + compute(); +} + +// Format a time interval given in nanoseconds. +static QString fmt_time_ns(double t_ns) +{ + const double a = fabs(t_ns); + if (a >= 1e9) + return QString::number(t_ns / 1e9, 'f', 3) + " s"; + if (a >= 1e6) + return QString::number(t_ns / 1e6, 'f', 3) + " ms"; + if (a >= 1e3) + return QString::number(t_ns / 1e3, 'f', 3) + " us"; + return QString::number(t_ns, 'f', 2) + " ns"; +} + +static QString fmt_freq_hz(double f_hz) +{ + const double a = fabs(f_hz); + if (a >= 1e6) + return QString::number(f_hz / 1e6, 'f', 3) + " MHz"; + if (a >= 1e3) + return QString::number(f_hz / 1e3, 'f', 3) + " kHz"; + return QString::number(f_hz, 'f', 2) + " Hz"; +} + +void DsoHistogram::compute() +{ + _value_plot->clear_data(); + _time_plot->clear_data(); + + int index = -1; + if (_ch_combobox->count() > 0) + index = _ch_combobox->itemData(_ch_combobox->currentIndex()).toInt(); + + view::DsoSignal *dsoSig = NULL; + for (auto s : _session->get_signals()) { + if (s->signal_type() == SR_CHANNEL_DSO) { + view::DsoSignal *d = (view::DsoSignal*)s; + if (d->get_index() == index) { + dsoSig = d; + break; + } + } + } + + if (dsoSig == NULL || !dsoSig->enabled()) { + _stats_label->setText( + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_HIST_NO_CHANNEL), + "No enabled DSO channel selected.")); + return; + } + + data::DsoSnapshot *data = dsoSig->data(); + if (data == NULL || data->empty()) { + _stats_label->setText( + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_HIST_NO_DATA), + "No captured data. Run an acquisition first.")); + return; + } + + const uint64_t total = data->get_sample_count(); + const uint8_t *buf = data->get_samples(0, 0, dsoSig->get_index()); + if (total < 2 || buf == NULL) { + _stats_label->setText( + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_HIST_NO_DATA), + "No captured data. Run an acquisition first.")); + return; + } + + // Raw-sample -> millivolt conversion, mirroring DsoSignal::get_voltage(). + const int hw_offset = dsoSig->get_hw_offset(); + const double k = data->get_measure_voltage_factor(dsoSig->get_index()); + const double data_scale = data->get_data_scale(dsoSig->get_index()); + const double vfactor = dsoSig->get_vDial()->get_factor(); + const int vrect_h = dsoSig->get_view_rect().height(); + const double vscale = (vrect_h > 0) + ? data_scale * k * vfactor * DS_CONF_DSO_VDIVS / vrect_h : 0.0; + + // Keep the dialog responsive on very deep captures. + const uint64_t MaxSamples = 8000000; + const uint64_t n = min(total, MaxSamples); + + auto volt = [&](uint64_t i) -> double { + return (hw_offset - (double)buf[i]) * vscale; + }; + + // --- value (amplitude) histogram + stats --- + double vmin = 1e300, vmax = -1e300, vsum = 0; + for (uint64_t i = 0; i < n; i++) { + const double v = volt(i); + vmin = min(vmin, v); + vmax = max(vmax, v); + vsum += v; + } + const double vmean = vsum / n; + const double vspan = (vmax > vmin) ? (vmax - vmin) : 1.0; + + // Selected display unit (values are computed in mV): factor + precision. + const double vfac = _unit_combobox->itemData( + _unit_combobox->currentIndex()).toDouble(); + const QString vunit = _unit_combobox->currentText(); + const int vprec = (vfac < 1.0) ? 4 : 2; // more decimals when showing V + + const int NBINS = 128; + QVector vbins(NBINS, 0.0); + for (uint64_t i = 0; i < n; i++) { + int b = (int)((volt(i) - vmin) / vspan * (NBINS - 1)); + b = max(0, min(NBINS - 1, b)); + vbins[b] += 1.0; + } + _value_plot->set_data(vbins, vmin * vfac, vmax * vfac, vunit, + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_HIST_VALUE), "Value distribution"), + true, vmean * vfac); + + // --- timing (jitter) histogram + stats --- + // Use the analysed snapshot's own sample rate (this is what the waveform + // painter uses); cur_snap_samplerate() reads the capture buffer, which can + // read back as 0 for the view buffer and silently disable jitter analysis. + double samplerate = data->samplerate(); + if (samplerate <= 0) + samplerate = _session->cur_snap_samplerate(); + const double dt_ns = (samplerate > 0) ? 1e9 / samplerate : 0.0; + + // Detect edges directly on the raw ADC samples (0..255). This keeps the + // jitter measurement independent of the voltage scaling, which can read + // back as zero in some states and would otherwise flatten the signal. + // (Fully qualified: the local variable "data" above shadows the "data" + // namespace within this function.) + const pv::data::DsoEdgeSet edge_set = pv::data::dso_detect_edges( + n, [&](uint64_t i) -> double { return (double)buf[i]; }); + const std::vector &edges = edge_set.rising; + + // Build the statistics as a two-column table (metric | value) so the + // amplitude and timing figures line up and read cleanly. + auto row = [](const QString &k, const QString &v) { + return QString("%1" + "%2").arg(k).arg(v); + }; + auto sep = []() { + return QString("
"); + }; + + auto vstr = [&](double mv_value) { + return QString::number(mv_value * vfac, 'f', vprec) + " " + vunit; + }; + + QString stats = ""; + stats += row(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_CHANNEL), "Channel"), + "CH" + dsoSig->get_name()); + stats += row("Samples", QString::number(n)); + stats += sep(); + stats += row("Vmax", vstr(vmax)); + stats += row("Vmin", vstr(vmin)); + stats += row("Vpp", vstr(vmax - vmin)); + stats += row("Vmean", vstr(vmean)); + + if (edges.size() >= 3 && dt_ns > 0) { + std::vector periods; // ns + periods.reserve(edges.size() - 1); + for (size_t e = 1; e < edges.size(); e++) + periods.push_back((edges[e] - edges[e - 1]) * dt_ns); + + double pmin = 1e300, pmax = -1e300, psum = 0; + for (double p : periods) { + pmin = min(pmin, p); + pmax = max(pmax, p); + psum += p; + } + const double pmean = psum / periods.size(); + double var = 0; + for (double p : periods) + var += (p - pmean) * (p - pmean); + const double pstd = sqrt(var / periods.size()); // RMS jitter + const double ppk = pmax - pmin; // pk-pk jitter + + const int TBINS = 128; + QVector tbins(TBINS, 0.0); + const double pspan = (pmax > pmin) ? (pmax - pmin) : 1.0; + for (double p : periods) { + int b = (int)((p - pmin) / pspan * (TBINS - 1)); + b = max(0, min(TBINS - 1, b)); + tbins[b] += 1.0; + } + _time_plot->set_data(tbins, pmin, pmax, "ns", + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_HIST_PERIOD), + "Period distribution (jitter)"), + true, pmean); + + stats += sep(); + stats += row("Edges", QString::number((qulonglong)edges.size())); + stats += row("Mean period", fmt_time_ns(pmean)); + stats += row("Frequency", fmt_freq_hz(1e9 / pmean)); + stats += row("RMS jitter", fmt_time_ns(pstd)); + stats += row("Pk-pk jitter", fmt_time_ns(ppk)); + } else { + stats += sep(); + stats += QString("").arg( + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_HIST_NO_JITTER), + "Not enough edges for jitter analysis.")); + stats += row("Edges found", QString::number((qulonglong)edges.size())); + } + + stats += "
%1
"; + _stats_label->setText(stats); +} + +} // namespace dialogs +} // namespace pv diff --git a/DSView/pv/dialogs/dsohistogram.h b/DSView/pv/dialogs/dsohistogram.h new file mode 100644 index 000000000..c945a7f48 --- /dev/null +++ b/DSView/pv/dialogs/dsohistogram.h @@ -0,0 +1,104 @@ +/* + * This file is part of the DSView project. + * DSView is based on PulseView. + * + * Copyright (C) 2026 Schildkroet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef DSVIEW_PV_DSOHISTOGRAM_H +#define DSVIEW_PV_DSOHISTOGRAM_H + +#include +#include +#include +#include +#include + +#include "dsdialog.h" +#include "../ui/dscombobox.h" + +namespace pv { + +class SigSession; + +namespace dialogs { + +// A small bar-chart widget that draws a single histogram (bin counts) with an +// x-axis annotated by its min/max value and a title. +class HistogramPlot : public QWidget +{ + Q_OBJECT + +public: + HistogramPlot(QWidget *parent = NULL); + + // has_mean/mean_value optionally draw a dashed marker line at that x + // position (e.g. the dataset's mean), which helps gauge how a distorted + // distribution sits relative to its average at a glance. + void set_data(const QVector &bins, double x_min, double x_max, + const QString &x_unit, const QString &title, + bool has_mean = false, double mean_value = 0.0); + void clear_data(); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + QString fmt_value(double v) const; + +private: + QVector _bins; + double _x_min; + double _x_max; + QString _x_unit; + QString _title; + bool _has_mean; + double _mean_value; +}; + +// Value + timing (jitter) histogram of a single DSO channel, with basic +// amplitude and jitter statistics. Computed once from the current snapshot. +class DsoHistogram : public DSDialog +{ + Q_OBJECT + +public: + DsoHistogram(SigSession *session, QWidget *parent); + ~DsoHistogram(); + +private slots: + void on_channel_changed(int index); + +private: + void compute(); + +private: + SigSession *_session; + + DsComboBox *_ch_combobox; + DsComboBox *_unit_combobox; + HistogramPlot *_value_plot; + HistogramPlot *_time_plot; + QLabel *_stats_label; + QVBoxLayout *_layout; + QDialogButtonBox _button_box; +}; + +} // namespace dialogs +} // namespace pv + +#endif // DSVIEW_PV_DSOHISTOGRAM_H diff --git a/DSView/pv/dialogs/mathoptions.cpp b/DSView/pv/dialogs/mathoptions.cpp index ac76ffda0..74529f942 100644 --- a/DSView/pv/dialogs/mathoptions.cpp +++ b/DSView/pv/dialogs/mathoptions.cpp @@ -3,6 +3,7 @@ * DSView is based on PulseView. * * Copyright (C) 2015 DreamSourceLab + * Copyright (C) 2026 Schildkroet * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -55,23 +56,38 @@ MathOptions::MathOptions(SigSession *session, QWidget *parent) : lisa_label->setPixmap(QPixmap(":/icons/math.svg")); _math_group = new QGroupBox(this); - QHBoxLayout *type_layout = new QHBoxLayout(); - QRadioButton *add_radio = new QRadioButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_ADD), "Add"), _math_group); - add_radio->setProperty("type", data::MathStack::MATH_ADD); - type_layout->addWidget(add_radio); - QRadioButton *sub_radio = new QRadioButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_SUBSTRACT), "Substract"), _math_group); - sub_radio->setProperty("type", data::MathStack::MATH_SUB); - type_layout->addWidget(sub_radio); - QRadioButton *mul_radio = new QRadioButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_MULTIPLY), "Multiply"), _math_group); - mul_radio->setProperty("type", data::MathStack::MATH_MUL); - type_layout->addWidget(mul_radio); - QRadioButton *div_radio = new QRadioButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_DIVIDE), "Divide"), _math_group); - div_radio->setProperty("type", data::MathStack::MATH_DIV); - type_layout->addWidget(div_radio); - _math_radio.append(add_radio); - _math_radio.append(sub_radio); - _math_radio.append(mul_radio); - _math_radio.append(div_radio); + QGridLayout *type_layout = new QGridLayout(); + + // Place operator radios in a 4-column grid (11 operators no longer fit on + // a single row). + int rrow = 0, rcol = 0; + auto place = [&](QRadioButton *b, int t) { + b->setProperty("type", t); + _math_radio.append(b); + type_layout->addWidget(b, rrow, rcol); + if (++rcol >= 4) { rcol = 0; rrow++; } + }; + + place(new QRadioButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_ADD), "Add"), _math_group), data::MathStack::MATH_ADD); + place(new QRadioButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_SUBSTRACT), "Substract"), _math_group), data::MathStack::MATH_SUB); + place(new QRadioButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_MULTIPLY), "Multiply"), _math_group), data::MathStack::MATH_MUL); + place(new QRadioButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_DIVIDE), "Divide"), _math_group), data::MathStack::MATH_DIV); + place(new QRadioButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_INTEGRATE), "Integrate"), _math_group), data::MathStack::MATH_INTEG); + place(new QRadioButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_DIFFERENTIATE), "Differentiate"), _math_group), data::MathStack::MATH_DIFF); + place(new QRadioButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_ABSOLUTE), "Absolute"), _math_group), data::MathStack::MATH_ABS); + place(new QRadioButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_SQUARE), "Square"), _math_group), data::MathStack::MATH_SQUARE); + place(new QRadioButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_SQRT), "Sqrt"), _math_group), data::MathStack::MATH_SQRT); + place(new QRadioButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_LOWPASS), "Low pass"), _math_group), data::MathStack::MATH_LOWPASS); + place(new QRadioButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_HIGHPASS), "High pass"), _math_group), data::MathStack::MATH_HIGHPASS); + + // Moving-average window (samples) used by the low/high-pass filters. + _filter_label = new QLabel(_math_group); + _filter_width = new QSpinBox(_math_group); + _filter_width->setRange(1, 100000); + _filter_width->setValue(10); + type_layout->addWidget(_filter_label, rrow + 1, 0, 1, 2); + type_layout->addWidget(_filter_width, rrow + 1, 2, 1, 2); + _math_group->setLayout(type_layout); _src1_group = new QGroupBox(this); @@ -104,6 +120,7 @@ MathOptions::MathOptions(SigSession *session, QWidget *parent) : auto math = _session->get_math_trace(); if (math) { _enable->setChecked(math->enabled()); + _filter_width->setValue(math->get_math_stack()->get_filter_width()); for (QVector::const_iterator i = _src1_radio.begin(); i != _src1_radio.end(); i++) { if ((*i)->property("index").toInt() == math->src1()) { @@ -171,6 +188,7 @@ void MathOptions::retranslateUi() { _enable->setText(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_ENABLE), "Enable")); _math_group->setTitle(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_MATH_TYPE), "Math Type")); + _filter_label->setText(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_FILTER_WIDTH), "Filter window (samples)")); _src1_group->setTitle(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_1ST_SOURCE), "1st Source")); _src2_group->setTitle(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_2ST_SOURCE), "2st Source")); setTitle(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_MATH_OPTIONS), "Math Options")); @@ -208,6 +226,11 @@ void MathOptions::Apply() break; } } + // Unary operators (integrate/differentiate/abs/square/sqrt/filters) only + // use the 1st source; fall back to it so a 2nd source need not be picked. + if (data::MathStack::is_unary(type) && src2 == -1) + src2 = src1; + bool enable = (src1 != -1 && src2 != -1 && _enable->isChecked()); view::DsoSignal *dsoSig1 = NULL; view::DsoSignal *dsoSig2 = NULL; @@ -223,8 +246,8 @@ void MathOptions::Apply() } if (dsoSig1 != NULL && dsoSig2 != NULL){ - _session->math_rebuild(enable, dsoSig1, dsoSig2, type); - } + _session->math_rebuild(enable, dsoSig1, dsoSig2, type, _filter_width->value()); + } } void MathOptions::reject() diff --git a/DSView/pv/dialogs/mathoptions.h b/DSView/pv/dialogs/mathoptions.h index 30b5c43bd..3a87691b5 100644 --- a/DSView/pv/dialogs/mathoptions.h +++ b/DSView/pv/dialogs/mathoptions.h @@ -3,6 +3,7 @@ * DSView is based on PulseView. * * Copyright (C) 2015 DreamSourceLab + * Copyright (C) 2026 Schildkroet * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -30,6 +31,8 @@ #include #include #include +#include +#include #include "../view/dsosignal.h" #include "../toolbars/titlebar.h" #include "dsdialog.h" @@ -80,6 +83,8 @@ class MathOptions : public DSDialog, public IUiWindow QVector _src1_radio; QVector _src2_radio; QVector _math_radio; + QLabel *_filter_label; + QSpinBox *_filter_width; QDialogButtonBox _button_box; QGridLayout *_layout; }; diff --git a/DSView/pv/dialogs/refoptions.cpp b/DSView/pv/dialogs/refoptions.cpp new file mode 100644 index 000000000..b09fde09a --- /dev/null +++ b/DSView/pv/dialogs/refoptions.cpp @@ -0,0 +1,123 @@ +/* + * This file is part of the DSView project. + * DSView is based on PulseView. + * + * Copyright (C) 2026 Schildkroet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "refoptions.h" + +#include +#include + +#include "../sigsession.h" +#include "../view/dsosignal.h" +#include "../ui/langresource.h" + +using namespace std; + +namespace pv { +namespace dialogs { + +RefOptions::RefOptions(SigSession *session, QWidget *parent) : + DSDialog(parent), + _session(session), + _button_box(QDialogButtonBox::Close, Qt::Horizontal, this) +{ + setMinimumSize(340, 180); + + _ch_combobox = new DsComboBox(this); + for (auto s : _session->get_signals()) { + if (s->signal_type() == SR_CHANNEL_DSO) { + view::DsoSignal *dsoSig = (view::DsoSignal*)s; + _ch_combobox->addItem(dsoSig->get_name(), + QVariant::fromValue(dsoSig->get_index())); + } + } + + _save_btn = new QPushButton( + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_REF_SAVE), "Save reference"), this); + _clear_btn = new QPushButton( + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_REF_CLEAR), "Clear all"), this); + _count_label = new QLabel(this); + + QFormLayout *ch_layout = new QFormLayout(); + ch_layout->addRow( + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_CHANNEL), "Channel"), _ch_combobox); + + QHBoxLayout *btn_layout = new QHBoxLayout(); + btn_layout->addWidget(_save_btn); + btn_layout->addWidget(_clear_btn); + + _layout = new QVBoxLayout(); + _layout->addLayout(ch_layout); + _layout->addLayout(btn_layout); + _layout->addWidget(_count_label); + _layout->addWidget(&_button_box); + + layout()->addLayout(_layout); + setTitle(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_REF_OPTIONS), "Reference Waveforms")); + + connect(&_button_box, SIGNAL(rejected()), this, SLOT(reject())); + connect(&_button_box, SIGNAL(accepted()), this, SLOT(accept())); + connect(_save_btn, SIGNAL(clicked()), this, SLOT(on_save())); + connect(_clear_btn, SIGNAL(clicked()), this, SLOT(on_clear())); + + update_count(); +} + +RefOptions::~RefOptions() +{ +} + +void RefOptions::update_count() +{ + _count_label->setText( + QString("%1: %2") + .arg(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_REF_STORED), "Stored references")) + .arg((int)_session->get_ref_waves().size())); +} + +void RefOptions::on_save() +{ + if (_ch_combobox->count() == 0) + return; + + const int index = + _ch_combobox->itemData(_ch_combobox->currentIndex()).toInt(); + + for (auto s : _session->get_signals()) { + if (s->signal_type() == SR_CHANNEL_DSO) { + view::DsoSignal *d = (view::DsoSignal*)s; + if (d->get_index() == index) { + _session->add_ref_wave(d); + break; + } + } + } + + update_count(); +} + +void RefOptions::on_clear() +{ + _session->clear_ref_waves(); + update_count(); +} + +} // namespace dialogs +} // namespace pv diff --git a/DSView/pv/dialogs/refoptions.h b/DSView/pv/dialogs/refoptions.h new file mode 100644 index 000000000..efc44b0ef --- /dev/null +++ b/DSView/pv/dialogs/refoptions.h @@ -0,0 +1,70 @@ +/* + * This file is part of the DSView project. + * DSView is based on PulseView. + * + * Copyright (C) 2026 Schildkroet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef DSVIEW_PV_REFOPTIONS_H +#define DSVIEW_PV_REFOPTIONS_H + +#include +#include +#include +#include + +#include "dsdialog.h" +#include "../ui/dscombobox.h" + +namespace pv { + +class SigSession; + +namespace dialogs { + +// Save the current DSO channel as a frozen reference waveform, or clear the +// stored references. References are overlaid on the live view. +class RefOptions : public DSDialog +{ + Q_OBJECT + +public: + RefOptions(SigSession *session, QWidget *parent); + ~RefOptions(); + +private slots: + void on_save(); + void on_clear(); + +private: + void update_count(); + +private: + SigSession *_session; + + DsComboBox *_ch_combobox; + QPushButton *_save_btn; + QPushButton *_clear_btn; + QLabel *_count_label; + QVBoxLayout *_layout; + QDialogButtonBox _button_box; +}; + +} // namespace dialogs +} // namespace pv + +#endif // DSVIEW_PV_REFOPTIONS_H diff --git a/DSView/pv/dialogs/storeprogress.cpp b/DSView/pv/dialogs/storeprogress.cpp index bb2685a2c..5341a46e2 100644 --- a/DSView/pv/dialogs/storeprogress.cpp +++ b/DSView/pv/dialogs/storeprogress.cpp @@ -179,8 +179,6 @@ void StoreProgress::accept() uint64_t start_index = 0; uint64_t end_index = 0; - auto &cursor_list = _view->get_cursorList(); - int dex1 = _start_cursor->currentIndex(); int dex2 = _end_cursor->currentIndex(); @@ -279,7 +277,7 @@ void StoreProgress::save_run(ISessionDataGetter *getter) auto &cursor_list = _view->get_cursorList(); - for (int i=0; iget_cursorList(); - for (int i=0; isetFixedSize(editline->size()); this->setFixedSize(editline->size()); - QPoint pt = mapToGlobal(editline->rect().bottomLeft()); - QPoint p1 = editline->pos(); QPoint p2 = editline->mapToGlobal(p1); int x = p2.x() - p1.x(); diff --git a/DSView/pv/dock/measuredock.cpp b/DSView/pv/dock/measuredock.cpp index a08de5727..81b03c78f 100644 --- a/DSView/pv/dock/measuredock.cpp +++ b/DSView/pv/dock/measuredock.cpp @@ -54,8 +54,9 @@ MeasureDock::MeasureDock(QWidget *parent, View &view, SigSession *session) : QScrollArea(parent), _session(session), _view(view) -{ - _widget = new QWidget(this); +{ + this->setWidgetResizable(true); + _widget = new QWidget(this); _dist_pannel = NULL; _edge_pannel = NULL; @@ -156,7 +157,6 @@ MeasureDock::MeasureDock(QWidget *parent, View &view, SigSession *session) : _widget->setLayout(layout); this->setWidget(_widget); - _widget->setGeometry(0, 0, sizeHint().width(), 2000); _widget->setObjectName("measureWidget"); add_dist_measure(); @@ -646,8 +646,6 @@ void MeasureDock::update_dist() { auto &cursor_list = _view.get_cursorList(); - QColor bkColor = AppConfig::Instance().GetStyleColor(); - auto mode_rows = get_mode_rows(); for (auto &inf : mode_rows->_dist_row_list) @@ -960,9 +958,6 @@ void MeasureDock::UpdateFont() font.setPointSizeF(font.pointSizeF() + 1); this->parentWidget()->setFont(font); - font.setStretch(QFont::Condensed); - _condensed_font = font; - adjusLabelSize(); } @@ -986,16 +981,15 @@ void MeasureDock::adjust_form_size(QWidget *wid) o->setFixedSize(size); } - QFontMetrics fm_condensed(_condensed_font); - _width_time_label->setFont(_condensed_font); - _width_samples_label->setFont(_condensed_font); - _period_time_label->setFont(_condensed_font); - _period_samples_label->setFont(_condensed_font); - _freq_label->setFont(_condensed_font); - _duty_label->setFont(_condensed_font); - int samples_label_width = fm_condensed.horizontalAdvance("############"); - int time_label_width = fm_condensed.horizontalAdvance("+12.345678999ms"); - int duty_label_width = fm_condensed.horizontalAdvance("+100.00% / +100.00%"); + _width_time_label->setFont(font); + _width_samples_label->setFont(font); + _period_time_label->setFont(font); + _period_samples_label->setFont(font); + _freq_label->setFont(font); + _duty_label->setFont(font); + int samples_label_width = fm.horizontalAdvance("############"); + int time_label_width = fm.horizontalAdvance("+12.345678999ms"); + int duty_label_width = fm.horizontalAdvance("+100.00% / +100.00%"); _width_time_label->setMinimumWidth(time_label_width); _width_time_label->setAlignment(Qt::AlignRight); _width_samples_label->setMinimumWidth(samples_label_width); @@ -1011,13 +1005,13 @@ void MeasureDock::adjust_form_size(QWidget *wid) auto groups = wid->findChildren(); for(auto o : groups) - { - o->setFixedWidth(max_label_width + 10); + { + o->setMinimumWidth(max_label_width + 10); } QWidget *pannel = dynamic_cast(mainGroup->parent()); - if (pannel != NULL){ - pannel->setFixedWidth(max_label_width + 20); + if (pannel != NULL){ + pannel->setMinimumWidth(max_label_width + 20); } } diff --git a/DSView/pv/dock/measuredock.h b/DSView/pv/dock/measuredock.h index 8bad194e5..65b76da25 100644 --- a/DSView/pv/dock/measuredock.h +++ b/DSView/pv/dock/measuredock.h @@ -178,8 +178,6 @@ public slots: QLabel *_f_label; QLabel *_d_label; bool _bSetting; - - QFont _condensed_font; }; } // namespace dock diff --git a/DSView/pv/dock/protocoldock.cpp b/DSView/pv/dock/protocoldock.cpp index 132474bd8..82dc995ef 100644 --- a/DSView/pv/dock/protocoldock.cpp +++ b/DSView/pv/dock/protocoldock.cpp @@ -480,6 +480,7 @@ void ProtocolDock::set_model() { pv::dialogs::ProtocolList *protocollist_dlg = new pv::dialogs::ProtocolList(this, _session); protocollist_dlg->exec(); + delete protocollist_dlg; resize_table_view(_session->get_decoder_model()); _model_proxy.setSourceModel(_session->get_decoder_model()); search_done(); @@ -614,6 +615,7 @@ void ProtocolDock::export_table_view() { pv::dialogs::ProtocolExp *protocolexp_dlg = new pv::dialogs::ProtocolExp(this, _session); protocolexp_dlg->exec(); + delete protocolexp_dlg; } void ProtocolDock::nav_table_view() @@ -1073,6 +1075,12 @@ void ProtocolDock::UpdateFont() ui::set_form_font(this, font); _table_view->setFont(font); + // The decoded-results model provides no size hint, so the table keeps its + // default row height and clips larger fonts. Grow the rows with the font, + // leaving room for cell margins and descenders. + QFontMetrics cell_fm(font); + _table_view->verticalHeader()->setDefaultSectionSize(qRound(cell_fm.height() * 1.5)); + for(auto lay : _protocol_lay_items){ lay->update_font(); } diff --git a/DSView/pv/dock/searchdock.cpp b/DSView/pv/dock/searchdock.cpp index 17e52268f..9d2f04938 100644 --- a/DSView/pv/dock/searchdock.cpp +++ b/DSView/pv/dock/searchdock.cpp @@ -182,7 +182,6 @@ void SearchDock::on_previous() _is_busy = false; }); - Qt::WindowFlags flags = Qt::CustomizeWindowHint; QString title = L_S(STR_PAGE_DLG, S_ID(IDS_DLG_SEARCH_PREVIOUS), "Search Previous..."); QString cancelText = L_S(STR_PAGE_DLG, S_ID(IDS_DLG_CANCEL), "Cancel"); EdgeSearchProgressDialog dlg(this, title, cancelText); @@ -245,7 +244,6 @@ void SearchDock::on_next() _is_busy = false; }); - Qt::WindowFlags flags = Qt::CustomizeWindowHint; QString title = L_S(STR_PAGE_DLG, S_ID(IDS_DLG_SEARCH_NEXT), "Search Next..."); QString cancelText = L_S(STR_PAGE_DLG, S_ID(IDS_DLG_CANCEL), "Cancel"); EdgeSearchProgressDialog dlg(this, title, cancelText); diff --git a/DSView/pv/dock/triggerdock.cpp b/DSView/pv/dock/triggerdock.cpp index 895060734..afa98b22b 100644 --- a/DSView/pv/dock/triggerdock.cpp +++ b/DSView/pv/dock/triggerdock.cpp @@ -64,7 +64,8 @@ TriggerDock::TriggerDock(QWidget *parent, SigSession *session) : QScrollArea(parent), _session(session) { - + this->setWidgetResizable(true); + _cur_ch_num = 16; if (_session->get_device()->have_instance()) { _session->get_device()->get_config_int16(SR_CONF_TOTAL_CH_NUM, _cur_ch_num); diff --git a/DSView/pv/mainframe.cpp b/DSView/pv/mainframe.cpp index acc3a5611..86b3ea16f 100644 --- a/DSView/pv/mainframe.cpp +++ b/DSView/pv/mainframe.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +47,7 @@ #include #include #include +#include #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #include @@ -63,6 +65,7 @@ #ifdef _WIN32 #include "winnativewidget.h" +#include #endif namespace pv { @@ -97,7 +100,7 @@ MainFrame::MainFrame() #ifdef _WIN32 setWindowFlags(Qt::FramelessWindowHint); _is_win32_parent_window = true; - _taskBtn = NULL; + _taskbarList3 = NULL; isWin32 = true; #else setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::WindowSystemMenuHint); @@ -179,7 +182,6 @@ MainFrame::MainFrame() } #ifdef _WIN32 - _taskBtn = new QWinTaskbarButton(this); connect(_mainWindow, SIGNAL(prgRate(int)), this, SLOT(setTaskbarProgress(int))); #endif @@ -193,9 +195,19 @@ MainFrame::MainFrame() connect(this, SIGNAL(sig_ParentNativeEvent(int)), this, SLOT(OnParentNaitveWindowEvent(int))); - + } - + +MainFrame::~MainFrame() +{ +#ifdef _WIN32 + if (_taskbarList3 != NULL) { + _taskbarList3->Release(); + _taskbarList3 = NULL; + } +#endif +} + void MainFrame::MoveWindow(int x, int y) { #ifdef _WIN32 @@ -251,7 +263,7 @@ void MainFrame::OnParentNativeEvent(ParentNativeEvent msg) void MainFrame::OnParentNaitveWindowEvent(int msg) { - + (void)msg; #ifdef _WIN32 if (_parentNativeWidget != NULL && msg == PARENT_EVENT_DISPLAY_CHANGED){ @@ -408,12 +420,8 @@ bool MainFrame::eventFilter(QObject *object, QEvent *event) { const QEvent::Type type = event->type(); const QMouseEvent *const mouse_event = (QMouseEvent*)event; - int newWidth = 0; - int newHeight = 0; - int newLeft = 0; - int newTop = 0; -#ifdef _WIN32 +#ifdef _WIN32 if (_parentNativeWidget != NULL){ return QFrame::eventFilter(object, event); } @@ -468,7 +476,11 @@ bool MainFrame::eventFilter(QObject *object, QEvent *event) QPoint pt; int k = 1; - pt = mouse_event->globalPos(); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + pt = mouse_event->globalPosition().toPoint(); +#else + pt = mouse_event->globalPos(); +#endif int datX = pt.x() - _clickPos.x(); int datY = pt.y() - _clickPos.y(); @@ -567,14 +579,42 @@ bool MainFrame::eventFilter(QObject *object, QEvent *event) } } else if (type == QEvent::MouseButtonPress) { - if (mouse_event->button() == Qt::LeftButton) - if (_hit_border != None) + if (mouse_event->button() == Qt::LeftButton && _hit_border != None) { + + // Wayland forbids clients from setting their own geometry, so the + // manual per-pixel resize below (computed from raw global mouse + // deltas) is a no-op/unreliable there, exactly like the manual + // window move was. Ask the compositor to perform the resize + // instead, via the same protocol native window-edge resizing uses. + if (QGuiApplication::platformName().startsWith("wayland", Qt::CaseInsensitive)) { + QWindow *win = windowHandle(); + if (win != NULL) { + static const QHash edgeMap = { + { TopLeft, Qt::TopEdge | Qt::LeftEdge }, + { Top, Qt::TopEdge }, + { TopRight, Qt::TopEdge | Qt::RightEdge }, + { Right, Qt::RightEdge }, + { BottomRight, Qt::BottomEdge | Qt::RightEdge }, + { Bottom, Qt::BottomEdge }, + { BottomLeft, Qt::BottomEdge | Qt::LeftEdge }, + { Left, Qt::LeftEdge }, + }; + win->startSystemResize(edgeMap.value(_hit_border)); + return true; + } + } + _bDraging = true; - _timer.start(50); + } + _timer.start(50); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + _clickPos = mouse_event->globalPosition().toPoint(); +#else _clickPos = mouse_event->globalPos(); +#endif _dragStartRegion = GetFormRegion(); - } + } else if (type == QEvent::MouseButtonRelease) { if (mouse_event->button() == Qt::LeftButton) { _bDraging = false; @@ -687,6 +727,22 @@ void MainFrame::ShowFormInit() resize(w, h); } + // Restore the dockwidget layout only after the window has actually reached its + // final size. move()/resize()/showMaximized() above can be asynchronous (the + // window manager negotiates the real geometry later), and + // QMainWindow::restoreState() sizes the docks relative to the window's size at + // the moment it is called, so calling it too early collapses the dock widths. + MainWindow *mainWindow = _mainWindow; + QTimer::singleShot(0, this, [mainWindow](){ + mainWindow->restore_dock(); + }); + + // Delayed past ShowHelpDocAsync()'s 300ms so the two one-time startup + // notices (help doc, driver hint) don't pop up on top of each other. + QTimer::singleShot(800, this, [this](){ + show_driver_hint_once(); + }); + if (!_is_win32_parent_window){ QFrame::show(); return; @@ -1009,11 +1065,9 @@ void MainFrame::ReadSettings() full_rect.width(), full_rect.height()); } - dsv_info("Normal region, x:%d, y:%d, w:%d, h:%d", + dsv_info("Normal region, x:%d, y:%d, w:%d, h:%d", _normalRegion.x, _normalRegion.y, _normalRegion.w, _normalRegion.h); - // restore dockwidgets - _mainWindow->restore_dock(); _titleBar->setRestoreButton(app.frameOptions.isMax); _initWndInfo.k = k; } @@ -1021,10 +1075,15 @@ void MainFrame::ReadSettings() #ifdef _WIN32 void MainFrame::showEvent(QShowEvent *event) { - // Taskbar Progress Effert for Win7 and Above - if (_taskBtn && _taskBtn->window() == NULL) { - _taskBtn->setWindow(windowHandle()); - _taskPrg = _taskBtn->progress(); + // Taskbar Progress Effert for Win7 and Above, via native ITaskbarList3 COM interface + if (_taskbarList3 == NULL) { + if (SUCCEEDED(CoCreateInstance(CLSID_TaskbarList, NULL, CLSCTX_INPROC_SERVER, + IID_ITaskbarList3, (void**)&_taskbarList3))) { + if (FAILED(_taskbarList3->HrInit())) { + _taskbarList3->Release(); + _taskbarList3 = NULL; + } + } } event->accept(); } @@ -1033,11 +1092,16 @@ void MainFrame::showEvent(QShowEvent *event) void MainFrame::setTaskbarProgress(int progress) { #ifdef _WIN32 + if (_taskbarList3 == NULL) + return; + + HWND hwnd = (HWND)winId(); + if (progress > 0) { - _taskPrg->setVisible(true); - _taskPrg->setValue(progress); + _taskbarList3->SetProgressState(hwnd, TBPF_NORMAL); + _taskbarList3->SetProgressValue(hwnd, (ULONGLONG)progress, 100); } else { - _taskPrg->setVisible(false); + _taskbarList3->SetProgressState(hwnd, TBPF_NOPROGRESS); } #else (void)progress; @@ -1096,6 +1160,89 @@ void MainFrame::show_doc() } } +void MainFrame::show_driver_hint_once() +{ + AppConfig &app = AppConfig::Instance(); + if (!app.userHistory.showDriverHint) + return; + + QString text; + +#ifdef _WIN32 + // Skip entirely if this is an installed copy from the Inno Setup + // installer (installer/windows/dsview.iss) - it already stages the + // WinUSB driver via pnputil during setup, so there's nothing to warn + // about. Only the portable zip (which can't run pnputil unelevated) + // needs the hint below. + if (QFile::exists(QCoreApplication::applicationDirPath() + + "/installed_via_setup.marker")) + return; + + // Unlike Linux (a permission bit), Windows needs an actual WinUSB driver + // bound to the device before libusb can open it at all - there is no + // generic "any USB device just works" path. The installer sets this up + // automatically; the portable zip build does not. + text = L_S(STR_PAGE_MSG, S_ID(IDS_MSG_DRIVER_HINT_WIN), + "DreamSourceLab hardware needs a WinUSB driver to be recognized by " + "Windows. If your device does not appear in the device list, install " + "a WinUSB driver for it (for example with the free 'Zadig' tool), " + "then reconnect the device."); +#else + // Skip entirely if the udev rule is already active system-wide (a + // .deb/source "make install" already places it there) - nothing to warn + // the user about in that case. + static const char *rule_paths[] = { + "/usr/lib/udev/rules.d/60-dreamsourcelab.rules", + "/lib/udev/rules.d/60-dreamsourcelab.rules", + "/etc/udev/rules.d/60-dreamsourcelab.rules", + }; + for (const char *p : rule_paths) { + if (QFile::exists(p)) + return; + } + + // A copy of the rule ships in share/DSView next to the executable in + // every Linux packaging (system install, .deb, AppImage - see the + // install() rule in CMakeLists.txt); a plain uninstalled dev build won't + // have it, so fall back to printing the one-line rule to create by hand. + const QString bundled = QCoreApplication::applicationDirPath() + + "/../share/DSView/DreamSourceLab.rules"; + + if (QFile::exists(bundled)) { + text = QString(L_S(STR_PAGE_MSG, S_ID(IDS_MSG_DRIVER_HINT_LINUX_BUNDLED), + "DreamSourceLab hardware needs a udev rule granting USB access, " + "which was not found on this system (this is normal when running " + "the AppImage). To install it, run:\n\n" + "sudo cp \"%1\" /etc/udev/rules.d/ && " + "sudo udevadm control --reload-rules")) + .arg(bundled); + } else { + text = L_S(STR_PAGE_MSG, S_ID(IDS_MSG_DRIVER_HINT_LINUX), + "DreamSourceLab hardware needs a udev rule granting USB access, " + "which was not found on this system. Create " + "/etc/udev/rules.d/60-dreamsourcelab.rules with the following " + "line, then run 'sudo udevadm control --reload-rules':\n\n" + "SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"2a0e\", MODE=\"0666\""); + } +#endif + + QMessageBox msg(this); + msg.setWindowTitle(L_S(STR_PAGE_MSG, S_ID(IDS_MSG_DRIVER_HINT_TITLE), + "Hardware driver note")); + msg.setText(text); + QPushButton *noMoreButton = msg.addButton( + L_S(STR_PAGE_MSG, S_ID(IDS_MSG_NOT_SHOW_AGAIN), "Not Show Again"), + QMessageBox::ActionRole); + msg.addButton(L_S(STR_PAGE_MSG, S_ID(IDS_MSG_IGNORE), "Ignore"), + QMessageBox::ActionRole); + msg.exec(); + + if (msg.clickedButton() == noMoreButton) { + app.userHistory.showDriverHint = false; + app.SaveHistory(); + } +} + QWidget* MainFrame::GetMainWindow() { return _mainWindow; @@ -1122,8 +1269,8 @@ bool MainFrame::nativeEvent(const QByteArray &eventType, void *message, MESSAGE_ case WM_NCLBUTTONDBLCLK: case WM_NCHITTEST: { - *result = long(SendMessageW(hwnd, - msg->message, msg->wParam, msg->lParam)); + *result = (MESSAGE_RESULT_TYPE)SendMessageW(hwnd, + msg->message, msg->wParam, msg->lParam); return true; } } diff --git a/DSView/pv/mainframe.h b/DSView/pv/mainframe.h index 0c2ec3ee9..1df3c7b6f 100644 --- a/DSView/pv/mainframe.h +++ b/DSView/pv/mainframe.h @@ -31,8 +31,7 @@ #include #ifdef _WIN32 -#include -#include +struct ITaskbarList3; #endif #include "toolbars/titlebar.h" @@ -40,8 +39,10 @@ #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) typedef qintptr *MESSAGE_RESULT_PTR; +typedef qintptr MESSAGE_RESULT_TYPE; #else typedef long *MESSAGE_RESULT_PTR; +typedef long MESSAGE_RESULT_TYPE; #endif namespace pv { @@ -98,7 +99,8 @@ class MainFrame : public: MainFrame(); - + ~MainFrame(); + void ShowFormInit(); void ShowHelpDocAsync(); @@ -142,6 +144,7 @@ public slots: void writeSettings(); void ReadSettings(); void AttachNativeWindow(); + void show_driver_hint_once(); //ITitleParent void MoveWindow(int x, int y) override; @@ -172,10 +175,9 @@ public slots: int _hit_border; QTimer _timer; bool _freezing; - // Taskbar Progress Effert for Win7 and Above + // Taskbar Progress Effert for Win7 and Above, via native ITaskbarList3 COM interface #ifdef _WIN32 - QWinTaskbarButton *_taskBtn; - QWinTaskbarProgress *_taskPrg; + ITaskbarList3 *_taskbarList3; #endif bool _is_win32_parent_window; diff --git a/DSView/pv/mainwindow.cpp b/DSView/pv/mainwindow.cpp index 52c6c00f4..902c0db66 100644 --- a/DSView/pv/mainwindow.cpp +++ b/DSView/pv/mainwindow.cpp @@ -110,13 +110,18 @@ namespace pv namespace{ QString tmp_file; + + // Bump this when the built-in dock layout changes so a windowState + // saved under an older layout is not restored over the new one. + const int DOCK_LAYOUT_VERSION = 1; } MainWindow::MainWindow(toolbars::TitleBar *title_bar, QWidget *parent) : QMainWindow(parent) { _msg = NULL; - _frame = parent; + _frame = parent; + _restoring_dock_layout = false; assert(title_bar); assert(_frame); @@ -225,10 +230,13 @@ namespace pv _search_widget = new dock::SearchDock(_search_dock, *_view, _session); _search_dock->setWidget(_search_widget); + // Put all the right-side docks into the same tab group instead of stacking + // them on top of each other (which forced scrolling to reach the lower ones). + setTabPosition(Qt::RightDockWidgetArea, QTabWidget::North); addDockWidget(Qt::RightDockWidgetArea, _protocol_dock); - addDockWidget(Qt::RightDockWidgetArea, _trigger_dock); - addDockWidget(Qt::RightDockWidgetArea, _dso_trigger_dock); - addDockWidget(Qt::RightDockWidgetArea, _measure_dock); + tabifyDockWidget(_protocol_dock, _trigger_dock); + tabifyDockWidget(_trigger_dock, _dso_trigger_dock); + tabifyDockWidget(_dso_trigger_dock, _measure_dock); addDockWidget(Qt::BottomDockWidgetArea, _search_dock); // event filter @@ -276,6 +284,7 @@ namespace pv connect(_trig_bar, SIGNAL(sig_search(bool)), this, SLOT(on_search(bool))); connect(_trig_bar, SIGNAL(sig_setTheme(QString)), this, SLOT(switchTheme(QString))); connect(_trig_bar, SIGNAL(sig_show_lissajous(bool)), _view, SLOT(show_lissajous(bool))); + connect(_trig_bar, &toolbars::TrigBar::sig_dso_split, _view, &view::View::set_dso_split_channels); // file toolbar connect(_file_bar, SIGNAL(sig_load_file(QString)), this, SLOT(on_load_file(QString))); @@ -438,7 +447,7 @@ namespace pv save_config_to_file(sessionFile); } - app.frameOptions.windowState = saveState(); + app.frameOptions.windowState = saveState(DOCK_LAYOUT_VERSION); app.SaveFrame(); } @@ -483,6 +492,9 @@ namespace pv { _protocol_dock->setVisible(visible); + if (visible && !_restoring_dock_layout) + _protocol_dock->raise(); + if (!visible) _view->setFocus(); } @@ -494,12 +506,18 @@ namespace pv _trigger_widget->update_view(); _trigger_dock->setVisible(visible); _dso_trigger_dock->setVisible(false); + + if (visible && !_restoring_dock_layout) + _trigger_dock->raise(); } else { _dso_trigger_widget->update_view(); _trigger_dock->setVisible(false); _dso_trigger_dock->setVisible(visible); + + if (visible && !_restoring_dock_layout) + _dso_trigger_dock->raise(); } if (!visible) @@ -510,6 +528,9 @@ namespace pv { _measure_dock->setVisible(visible); + if (visible && !_restoring_dock_layout) + _measure_dock->raise(); + if (!visible) _view->setFocus(); } @@ -539,9 +560,9 @@ namespace pv (void)x; (void)y; -#ifdef _WIN32 +#ifdef _WIN32 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - QPixmap pixmap = QGuiApplication::primaryScreen()->grabWindow(QApplication::desktop->winId(), x, y, w, h); + QPixmap pixmap = parentWidget()->grab(); #else QPixmap pixmap = QPixmap::grabWidget(parentWidget()); #endif @@ -1206,7 +1227,9 @@ namespace pv { try { - restoreState(st); + // Versioned so a windowState saved before the right-side docks + // were tabified doesn't restore the old split (scrolling) layout. + restoreState(st, DOCK_LAYOUT_VERSION); } catch (...) { @@ -1215,8 +1238,11 @@ namespace pv } // Resotre the dock pannel. - if (_device_agent->have_instance()) + if (_device_agent->have_instance()){ + _restoring_dock_layout = true; _trig_bar->reload(); + _restoring_dock_layout = false; + } } bool MainWindow::eventFilter(QObject *object, QEvent *event) @@ -1397,12 +1423,12 @@ namespace pv if (language == LAN_CN) { - _qtTrans.load(":/qt_" + QString::number(language)); + (void)_qtTrans.load(":/qt_" + QString::number(language)); qApp->installTranslator(&_qtTrans); - _myTrans.load(":/my_" + QString::number(language)); + (void)_myTrans.load(":/my_" + QString::number(language)); qApp->installTranslator(&_myTrans); } - else if (language == LAN_EN) + else if (language == LAN_EN || language == LAN_DE) { qApp->removeTranslator(&_qtTrans); qApp->removeTranslator(&_myTrans); @@ -1618,12 +1644,18 @@ namespace pv } bool MainWindow::confirm_to_store_data() - { + { bool ret = false; - _is_save_confirm_msg = true; + _is_save_confirm_msg = true; + + if (AppConfig::Instance().appOptions.dontAskSaveOnExit) + { + _is_save_confirm_msg = false; + return false; + } if (_session->have_hardware_data() && _session->is_first_store_confirm()) - { + { // Only popup one time. ret = MsgBox::Confirm(L_S(STR_PAGE_MSG, S_ID(IDS_MSG_SAVE_CAPDATE), "Save captured data?")); @@ -2135,6 +2167,10 @@ namespace pv case DSV_MSG_APP_OPTIONS_CHANGED: { update_title_bar_text(); + // Recompute trace layout: the inter-channel spacing depends on + // the channel-divider option (see View::get_signal_margin()). + _view->signals_changed(NULL); + _view->viewport_update(); break; } case DSV_MSG_FONT_OPTIONS_CHANGED: diff --git a/DSView/pv/mainwindow.h b/DSView/pv/mainwindow.h index a43e04271..f1ec746cc 100644 --- a/DSView/pv/mainwindow.h +++ b/DSView/pv/mainwindow.h @@ -229,6 +229,12 @@ private slots: QDockWidget *_search_dock; dock::SearchDock *_search_widget; + // While true, on_protocol/on_trigger/on_measure won't raise() their dock when + // shown, so the tab that QMainWindow::restoreState() already made active (from + // a prior session) isn't stolen by the fixed-order visibility sync in + // TrigBar::reload() during startup. + bool _restoring_dock_layout; + QTranslator _qtTrans; QTranslator _myTrans; EventObject _event; diff --git a/DSView/pv/prop/binding/binding.cpp b/DSView/pv/prop/binding/binding.cpp index fb5f9f106..ed7af3428 100644 --- a/DSView/pv/prop/binding/binding.cpp +++ b/DSView/pv/prop/binding/binding.cpp @@ -38,7 +38,15 @@ const std::vector& Binding::properties() } Binding::Binding(){ - _row_num = 0; + _row_num = 0; +} + +Binding::~Binding() +{ + for(auto p : _properties) { + delete p; + } + _properties.clear(); } void Binding::commit() diff --git a/DSView/pv/prop/binding/binding.h b/DSView/pv/prop/binding/binding.h index 22f94c01a..1a35561c3 100644 --- a/DSView/pv/prop/binding/binding.h +++ b/DSView/pv/prop/binding/binding.h @@ -46,6 +46,8 @@ class Binding public: Binding(); + virtual ~Binding(); + const std::vector& properties(); void commit(); diff --git a/DSView/pv/prop/binding/decoderoptions.cpp b/DSView/pv/prop/binding/decoderoptions.cpp index f5da31378..6d85c99f4 100644 --- a/DSView/pv/prop/binding/decoderoptions.cpp +++ b/DSView/pv/prop/binding/decoderoptions.cpp @@ -22,7 +22,7 @@ #include #include "decoderoptions.h" -#include +#include #include #include "../../data/decoderstack.h" @@ -35,8 +35,9 @@ #include "../../config/appconfig.h" using namespace boost; +using namespace boost::placeholders; using namespace std; - + namespace pv { namespace prop { namespace binding { diff --git a/DSView/pv/prop/binding/deviceoptions.cpp b/DSView/pv/prop/binding/deviceoptions.cpp index 3ad6812d4..602a60a23 100644 --- a/DSView/pv/prop/binding/deviceoptions.cpp +++ b/DSView/pv/prop/binding/deviceoptions.cpp @@ -22,7 +22,7 @@ #include "deviceoptions.h" -#include +#include #include #include #include "../bool.h" @@ -35,8 +35,9 @@ #include "../../sigsession.h" #include "../../deviceagent.h" #include "../../ui/langresource.h" - + using namespace std; +using namespace boost::placeholders; namespace pv { namespace prop { diff --git a/DSView/pv/prop/binding/probeoptions.cpp b/DSView/pv/prop/binding/probeoptions.cpp index 3d9ac3ef4..804ae45b1 100644 --- a/DSView/pv/prop/binding/probeoptions.cpp +++ b/DSView/pv/prop/binding/probeoptions.cpp @@ -20,7 +20,7 @@ */ #include "probeoptions.h" -#include +#include #include #include #include "../bool.h" @@ -34,6 +34,7 @@ #include "../../ui/langresource.h" using namespace std; +using namespace boost::placeholders; namespace pv { namespace prop { diff --git a/DSView/pv/prop/enum.cpp b/DSView/pv/prop/enum.cpp index 78e2a6f75..1b8cb5695 100644 --- a/DSView/pv/prop/enum.cpp +++ b/DSView/pv/prop/enum.cpp @@ -70,16 +70,24 @@ QWidget* Enum::get_widget(QWidget *parent, bool auto_commit) _selector = new DsComboBox(parent); + int max_text_width = 0; + for (unsigned int i = 0; i < _values.size(); i++) { const pair &v = _values[i]; _selector->addItem(v.second, QVariant::fromValue((void*)v.first)); - + max_text_width = qMax(max_text_width, + _selector->fontMetrics().boundingRect(v.second).width()); + if (value && g_variant_compare(v.first, value) == 0) _selector->setCurrentIndex(i); } g_variant_unref(value); + _selector->setMinimumWidth(max_text_width + 40); + _selector->view()->setMinimumWidth(max_text_width + 40); + _selector->view()->setTextElideMode(Qt::ElideNone); + if (auto_commit) { connect(_selector, SIGNAL(currentIndexChanged(int)), this, SLOT(on_current_item_changed(int))); diff --git a/DSView/pv/sigsession.cpp b/DSView/pv/sigsession.cpp index 2546e6da4..f48b2b240 100644 --- a/DSView/pv/sigsession.cpp +++ b/DSView/pv/sigsession.cpp @@ -4,6 +4,7 @@ * * Copyright (C) 2012 Joel Holdsworth * Copyright (C) 2013 DreamSourceLab + * Copyright (C) 2026 Schildkroet * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1781,7 +1782,8 @@ namespace pv void SigSession::math_rebuild(bool enable, view::DsoSignal *dsoSig1, view::DsoSignal *dsoSig2, - data::MathStack::MathType type) + data::MathStack::MathType type, + int filter_width) { ds_lock_guard lock(_data_mutex); @@ -1790,7 +1792,8 @@ namespace pv DESTROY_OBJECT(_math_trace); - auto math_stack = new data::MathStack(this, dsoSig1, dsoSig2, type); + auto math_stack = new data::MathStack(this, dsoSig1, dsoSig2, type); + math_stack->set_filter_width(filter_width); _math_trace = new view::MathTrace(enable, math_stack, dsoSig1, dsoSig2); if (_math_trace && _math_trace->enabled()) @@ -1811,6 +1814,40 @@ namespace pv _math_trace->set_enable(false); } + void SigSession::add_ref_wave(view::DsoSignal *sig) + { + if (sig == NULL) + return; + + data::DsoSnapshot *data = sig->data(); + if (data == NULL || data->empty()) + return; + + const uint64_t n = data->get_sample_count(); + const uint8_t *buf = data->get_samples(0, 0, sig->get_index()); + if (n == 0 || buf == NULL) + return; + + RefWave rw; + rw.index = sig->get_index(); + rw.samples.assign(buf, buf + n); + rw.samplerate = data->samplerate(); + rw.colour = sig->get_colour(); + rw.name = QString("Ref %1").arg((int)_ref_waves.size() + 1); + _ref_waves.push_back(rw); + + signals_changed(); + } + + void SigSession::clear_ref_waves() + { + if (_ref_waves.empty()) + return; + + _ref_waves.clear(); + signals_changed(); + } + void SigSession::nodata_timeout() { int flag; @@ -2475,9 +2512,10 @@ namespace pv } void SigSession::clear_signals() - { + { DESTROY_OBJECT(_math_trace); - + _ref_waves.clear(); + for (int i=0; i< (int)_signals.size(); i++) { auto *p = _signals[i]; diff --git a/DSView/pv/sigsession.h b/DSView/pv/sigsession.h index 18d72f5fc..d3799d540 100644 --- a/DSView/pv/sigsession.h +++ b/DSView/pv/sigsession.h @@ -4,6 +4,7 @@ * * Copyright (C) 2012 Joel Holdsworth * Copyright (C) 2013 DreamSourceLab + * Copyright (C) 2026 Schildkroet * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -26,9 +27,11 @@ #include #include #include -#include +#include #include +#include #include +#include #include #include @@ -238,7 +241,26 @@ class SigSession: inline view::MathTrace* get_math_trace(){ return _math_trace; } - + + // ---- Reference waveforms ------------------------------------------------ + // A frozen copy of a DSO channel's samples, overlaid on the live view. It + // is rendered with the source channel's current vertical scaling (looked + // up by index at paint time) so it stays aligned to the grid. + struct RefWave { + int index; // source DSO channel index + std::vector samples; + double samplerate; + QColor colour; + QString name; + }; + + // Snapshot the given DSO channel into a new reference waveform. + void add_ref_wave(view::DsoSignal *sig); + void clear_ref_waves(); + inline std::vector& get_ref_waves(){ + return _ref_waves; + } + uint16_t get_ch_num(int type); inline bool is_data_lock(){ @@ -254,7 +276,8 @@ class SigSession: void math_rebuild(bool enable,pv::view::DsoSignal *dsoSig1, pv::view::DsoSignal *dsoSig2, - data::MathStack::MathType type); + data::MathStack::MathType type, + int filter_width = 10); inline bool trigd(){ return _trigger_flag; @@ -573,6 +596,7 @@ class SigSession: std::vector _spectrum_traces; view::LissajousTrace *_lissajous_trace; view::MathTrace *_math_trace; + std::vector _ref_waves; DsTimer _feed_timer; DsTimer _out_timer; @@ -583,13 +607,13 @@ class SigSession: int _noData_cnt; bool _data_lock; - bool _data_updated; + std::atomic _data_updated; int _data_auto_lock; QDateTime _session_time; QDateTime _trig_time; - bool _is_triged; - bool _trigger_flag; + std::atomic _is_triged; + std::atomic _trigger_flag; uint8_t _trigger_ch; bool _hw_replied; @@ -599,24 +623,24 @@ class SigSession: bool _bClose; uint64_t _save_start; - uint64_t _save_end; - volatile bool _is_working; + uint64_t _save_end; + std::atomic _is_working; double _repeat_intvl; // The progress wait timer interval. int _repeat_hold_prg; // The time sleep progress int _repeat_wait_prog_step; bool _is_saving; bool _is_instant; - volatile int _device_status; + std::atomic _device_status; int _work_time_id; - int _capture_times; + int _capture_times; int _confirm_store_time_id; uint64_t _rt_refresh_time_id; uint64_t _rt_ck_refresh_time_id; DEVICE_COLLECT_MODE _clt_mode; bool _is_stream_mode; - + bool _is_action; - uint64_t _dso_packet_count; + std::atomic _dso_packet_count; bool _is_task_end; diff --git a/DSView/pv/storesession.cpp b/DSView/pv/storesession.cpp index fe0ff219a..11d96a334 100644 --- a/DSView/pv/storesession.cpp +++ b/DSView/pv/storesession.cpp @@ -316,7 +316,7 @@ void StoreSession::save_logic(pv::data::LogicSnapshot *logic_snapshot) block_buf = (uint8_t *)malloc(block_size); if (block_buf == NULL) { _has_error = true; - _error = L_S(STR_PAGE_DLG, S_ID(IDS_MSG_STORESESS_SAVEPROC_ERROR1), + _error = L_S(STR_PAGE_MSG, S_ID(IDS_MSG_STORESESS_SAVEPROC_ERROR1), "Failed to create zip file. Malloc error."); } else { @@ -330,7 +330,7 @@ void StoreSession::save_logic(pv::data::LogicSnapshot *logic_snapshot) if (ret != SR_OK) { if (!_has_error) { _has_error = true; - _error = L_S(STR_PAGE_DLG, S_ID(IDS_MSG_STORESESS_SAVEPROC_ERROR2), + _error = L_S(STR_PAGE_MSG, S_ID(IDS_MSG_STORESESS_SAVEPROC_ERROR2), "Failed to create zip file. Please check write permission of this path."); } progress_updated(); @@ -410,7 +410,7 @@ void StoreSession::save_analog(pv::data::AnalogSnapshot *analog_snapshot) uint8_t *tmp = (uint8_t *)malloc(size); if (tmp == NULL) { _has_error = true; - _error = L_S(STR_PAGE_DLG, S_ID(IDS_MSG_STORESESS_SAVEPROC_ERROR1), + _error = L_S(STR_PAGE_MSG, S_ID(IDS_MSG_STORESESS_SAVEPROC_ERROR1), "Failed to create zip file. Malloc error."); } else { memcpy(tmp, buf, buf_end-buf); @@ -434,7 +434,7 @@ void StoreSession::save_analog(pv::data::AnalogSnapshot *analog_snapshot) if (ret != SR_OK) { if (!_has_error) { _has_error = true; - _error = L_S(STR_PAGE_DLG, S_ID(IDS_MSG_STORESESS_SAVEPROC_ERROR2), + _error = L_S(STR_PAGE_MSG, S_ID(IDS_MSG_STORESESS_SAVEPROC_ERROR2), "Failed to create zip file. Please check write permission of this path."); } progress_updated(); @@ -491,7 +491,7 @@ void StoreSession::save_dso(pv::data::DsoSnapshot *dso_snapshot) if (ret != SR_OK) { if (!_has_error) { _has_error = true; - _error = L_S(STR_PAGE_DLG, S_ID(IDS_MSG_STORESESS_SAVEPROC_ERROR2), + _error = L_S(STR_PAGE_MSG, S_ID(IDS_MSG_STORESESS_SAVEPROC_ERROR2), "Failed to create zip file. Please check write permission of this path."); } progress_updated(); @@ -555,7 +555,6 @@ bool StoreSession::meta_gen(data::Snapshot *snapshot, std::string &str) struct sr_channel *probe; int probecnt; char *s; - struct sr_status status; char meta[300] = {0}; sprintf(meta, "%s", "[version]\n"); str += meta; @@ -811,11 +810,11 @@ bool StoreSession::export_start() } if (type_set.size() > 1) { - _error = L_S(STR_PAGE_DLG, S_ID(IDS_MSG_STORESESS_EXPORTSTART_ERROR1), + _error = L_S(STR_PAGE_MSG, S_ID(IDS_MSG_STORESESS_EXPORTSTART_ERROR1), "DSView does not currently support\nfile export for multiple data types."); return false; } else if (type_set.size() == 0) { - _error = L_S(STR_PAGE_DLG, S_ID(IDS_MSG_STORESESS_EXPORTSTART_ERROR2), "No data to save."); + _error = L_S(STR_PAGE_MSG, S_ID(IDS_MSG_STORESESS_EXPORTSTART_ERROR2), "No data to save."); return false; } @@ -823,12 +822,12 @@ bool StoreSession::export_start() assert(snapshot); // Check we have data if (snapshot->empty()) { - _error = L_S(STR_PAGE_DLG, S_ID(IDS_MSG_STORESESS_EXPORTSTART_ERROR2), "No data to save."); + _error = L_S(STR_PAGE_MSG, S_ID(IDS_MSG_STORESESS_EXPORTSTART_ERROR2), "No data to save."); return false; } if (_file_name == ""){ - _error = L_S(STR_PAGE_DLG, S_ID(IDS_MSG_STORESESS_EXPORTSTART_ERROR3), "No set file name."); + _error = L_S(STR_PAGE_MSG, S_ID(IDS_MSG_STORESESS_EXPORTSTART_ERROR3), "No set file name."); return false; } @@ -847,7 +846,7 @@ bool StoreSession::export_start() if (_outModule == NULL) { - _error = L_S(STR_PAGE_DLG, S_ID(IDS_MSG_STORESESS_EXPORTSTART_ERROR4), "Invalid export format."); + _error = L_S(STR_PAGE_MSG, S_ID(IDS_MSG_STORESESS_EXPORTSTART_ERROR4), "Invalid export format."); } else { @@ -895,7 +894,7 @@ void StoreSession::export_exec(data::Snapshot *snapshot) channel_type = SR_CHANNEL_ANALOG; } else { _has_error = true; - _error = L_S(STR_PAGE_DLG, S_ID(IDS_MSG_STORESESS_EXPORTPROC_ERROR1), "data type don't support."); + _error = L_S(STR_PAGE_MSG, S_ID(IDS_MSG_STORESESS_EXPORTPROC_ERROR1), "data type don't support."); return; } @@ -915,19 +914,36 @@ void StoreSession::export_exec(data::Snapshot *snapshot) output.start_sample_index = _start_index; } + auto release_export_params = [&](){ + g_hash_table_destroy(params); + if (filenameGVariant != NULL) + g_variant_unref(filenameGVariant); + if (typeGVariant != NULL) + g_variant_unref(typeGVariant); + }; + if(_outModule->init){ if(_outModule->init(&output, params) != SR_OK){ dsv_err("Failed to init export module."); + release_export_params(); return; } } - + QString dateTimeString = Formatting::DateTimeToString(_session->get_session_time(), TimeStrigFormatType::TIME_STR_FORMAT_ALL); strcpy(output.time_string, dateTimeString.toStdString().c_str()); - + QFile file(_file_name); - file.open(QIODevice::WriteOnly | QIODevice::Text); - QTextStream out(&file); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)){ + dsv_err("StoreSession::export_proc, failed to open file for writing."); + _has_error = true; + _error = QString("Failed to open file for writing: %1").arg(_file_name); + _outModule->cleanup(&output); + release_export_params(); + progress_updated(); + return; + } + QTextStream out(&file); encoding::set_utf8(out); //out.setGenerateByteOrderMark(true); // UTF-8 without BOM @@ -1005,6 +1021,9 @@ void StoreSession::export_exec(data::Snapshot *snapshot) if (start_index > logic_snapshot->get_ring_sample_count()){ dsv_err("ERROR:the start curosr is invalid!"); _units_stored = -1; + file.close(); + _outModule->cleanup(&output); + release_export_params(); progress_updated(); return; } @@ -1077,7 +1096,11 @@ void StoreSession::export_exec(data::Snapshot *snapshot) uint8_t *xbuf = (uint8_t *)malloc(size * unitsize); if (xbuf == NULL) { _has_error = true; - _error = L_S(STR_PAGE_DLG, S_ID(IDS_MSG_STORESESS_EXPORTPROC_ERROR2), "xbuffer malloc failed."); + _error = L_S(STR_PAGE_MSG, S_ID(IDS_MSG_STORESESS_EXPORTPROC_ERROR2), "xbuffer malloc failed."); + file.close(); + _outModule->cleanup(&output); + release_export_params(); + progress_updated(); return; } @@ -1190,8 +1213,6 @@ void StoreSession::export_exec(data::Snapshot *snapshot) void* data_buffer = analog_snapshot->get_data(); unsigned int usize = 8192; struct sr_datafeed_analog ap; - - unsigned char* read_buf = (unsigned char*)data_buffer; const uint64_t ring_start = analog_snapshot->get_ring_start(); @@ -1248,9 +1269,7 @@ void StoreSession::export_exec(data::Snapshot *snapshot) // optional, as QFile destructor will already do it: file.close(); _outModule->cleanup(&output); - g_hash_table_destroy(params); - if (filenameGVariant != NULL) - g_variant_unref(filenameGVariant); + release_export_params(); progress_updated(); } diff --git a/DSView/pv/toolbars/filebar.cpp b/DSView/pv/toolbars/filebar.cpp index f562431ce..a1ca34c55 100644 --- a/DSView/pv/toolbars/filebar.cpp +++ b/DSView/pv/toolbars/filebar.cpp @@ -19,7 +19,6 @@ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -#include #include #include #include diff --git a/DSView/pv/toolbars/logobar.cpp b/DSView/pv/toolbars/logobar.cpp index 4b3b492cc..9a2093138 100644 --- a/DSView/pv/toolbars/logobar.cpp +++ b/DSView/pv/toolbars/logobar.cpp @@ -19,9 +19,6 @@ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -#include - - #include #include #include @@ -67,17 +64,22 @@ LogoBar::LogoBar(SigSession *session, QWidget *parent) : _action_en = new QAction(this); _action_en->setObjectName(QString::fromUtf8("actionEn")); - + _action_cn = new QAction(this); _action_cn->setObjectName(QString::fromUtf8("actionCn")); - + + _action_de = new QAction(this); + _action_de->setObjectName(QString::fromUtf8("actionDe")); + _language = new QMenu(this); _language->setObjectName(QString::fromUtf8("menuLanguage")); _language->addAction(_action_cn); _language->addAction(_action_en); + _language->addAction(_action_de); _action_en->setIcon(QIcon(":/icons/English.svg")); _action_cn->setIcon(QIcon(":/icons/Chinese.svg")); + _action_de->setIcon(QIcon(":/icons/German.svg")); _about = new QAction(this); _about->setObjectName(QString::fromUtf8("actionAbout")); @@ -116,6 +118,7 @@ LogoBar::LogoBar(SigSession *session, QWidget *parent) : connect(_action_en, SIGNAL(triggered()), this, SLOT(on_actionEn_triggered())); connect(_action_cn, SIGNAL(triggered()), this, SLOT(on_actionCn_triggered())); + connect(_action_de, SIGNAL(triggered()), this, SLOT(on_actionDe_triggered())); connect(_about, SIGNAL(triggered()), this, SLOT(on_actionAbout_triggered())); connect(_manual, SIGNAL(triggered()), this, SIGNAL(sig_open_doc())); connect(_issue, SIGNAL(triggered()), this, SLOT(on_actionIssue_triggered())); @@ -136,16 +139,19 @@ void LogoBar::retranslateUi() _logo_button.setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_HELP), "Help")); _language->setTitle(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_HELP_LANG), "&Language")); _action_en->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_HELP_LANG_EN), "English")); - _action_cn->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_HELP_LANG_CN), "中文")); + _action_cn->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_HELP_LANG_CN), "中文")); + _action_de->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_HELP_LANG_DE), "Deutsch")); _about->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_HELP_ABOUT), "&About...")); _manual->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_HELP_MANUAL), "&Manual...")); _issue->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_HELP_BUG), "&Bug Report")); _update->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_HELP_UPDATE), "&Update")); _log->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_HELP_LOG), "L&og Options")); - AppConfig &app = AppConfig::Instance(); + AppConfig &app = AppConfig::Instance(); if (app.frameOptions.language == LAN_CN) _language->setIcon(QIcon(":/icons/Chinese.svg")); + else if (app.frameOptions.language == LAN_DE) + _language->setIcon(QIcon(":/icons/German.svg")); else _language->setIcon(QIcon(":/icons/English.svg")); } @@ -190,7 +196,15 @@ void LogoBar::on_actionCn_triggered() _language->setIcon(QIcon::fromTheme("file", QIcon(":/icons/Chinese.svg"))); assert(_mainForm); - _mainForm->switchLanguage(LAN_CN); + _mainForm->switchLanguage(LAN_CN); +} + +void LogoBar::on_actionDe_triggered() +{ + _language->setIcon(QIcon::fromTheme("file", + QIcon(":/icons/German.svg"))); + assert(_mainForm); + _mainForm->switchLanguage(LAN_DE); } void LogoBar::on_actionAbout_triggered() @@ -212,12 +226,7 @@ void LogoBar::on_actionIssue_triggered() void LogoBar::on_action_update() { - if (AppConfig::Instance().frameOptions.language == LAN_CN){ - QDesktopServices::openUrl(QUrl(QLatin1String("https://dreamsourcelab.cn/download/"))); - } - else{ - QDesktopServices::openUrl(QUrl(QLatin1String("https://www.dreamsourcelab.com/download/"))); - } + QDesktopServices::openUrl(QUrl(QLatin1String("https://github.com/Schildkroet/DSView/releases"))); } void LogoBar::enable_toggle(bool enable) @@ -283,6 +292,9 @@ void LogoBar::on_action_setting_log() dlg.exec(); + _log_open_bt = NULL; + _log_clear_bt = NULL; + if (dlg.IsClickYes()){ bool ableSave = ckSave->isChecked(); int level = cbBox->currentIndex(); diff --git a/DSView/pv/toolbars/logobar.h b/DSView/pv/toolbars/logobar.h index 19fea7fde..14a6a0495 100644 --- a/DSView/pv/toolbars/logobar.h +++ b/DSView/pv/toolbars/logobar.h @@ -72,6 +72,7 @@ class LogoBar : public QToolBar, public IUiWindow private slots: void on_actionEn_triggered(); void on_actionCn_triggered(); + void on_actionDe_triggered(); void on_actionAbout_triggered(); void on_actionManual_triggered(); void on_actionIssue_triggered(); @@ -92,6 +93,7 @@ private slots: QMenu *_language; QAction *_action_en; QAction *_action_cn; + QAction *_action_de; QAction *_about; QAction *_manual; diff --git a/DSView/pv/toolbars/samplingbar.cpp b/DSView/pv/toolbars/samplingbar.cpp index 29ba23e75..73bc61d1f 100644 --- a/DSView/pv/toolbars/samplingbar.cpp +++ b/DSView/pv/toolbars/samplingbar.cpp @@ -1139,6 +1139,7 @@ namespace pv _device_selector.clear(); + int max_text_width = 0; for (int i = 0; i < dev_count; i++) { p = (array + i); @@ -1168,7 +1169,7 @@ namespace pv } _last_device_index = select_index; - const int width = max_text_width + 20; + const int width = max_text_width + 30; const int selector_width = min(width, ComboBoxMaxWidth); const int popup_width = width; diff --git a/DSView/pv/toolbars/samplingbar.h b/DSView/pv/toolbars/samplingbar.h index cc0de0419..4485d2813 100644 --- a/DSView/pv/toolbars/samplingbar.h +++ b/DSView/pv/toolbars/samplingbar.h @@ -65,7 +65,7 @@ namespace pv Q_OBJECT private: - static const int ComboBoxMaxWidth = 320; + static const int ComboBoxMaxWidth = 420; static const int RefreshShort = 500; static const uint64_t LogicMaxSWDepth64 = SR_GB(16); static const uint64_t LogicMaxSWDepth32 = SR_GB(8); diff --git a/DSView/pv/toolbars/titlebar.cpp b/DSView/pv/toolbars/titlebar.cpp index 79ef01b55..4954af9b6 100644 --- a/DSView/pv/toolbars/titlebar.cpp +++ b/DSView/pv/toolbars/titlebar.cpp @@ -21,16 +21,18 @@ #include "titlebar.h" #include -#include +#include #include #include #include -#include +#include #include #include #include #include #include +#include +#include #include "../config/appconfig.h" #include "../appcontrol.h" @@ -43,7 +45,7 @@ namespace toolbars { TitleBar::TitleBar(bool top, QWidget *parent, ITitleParent *titleParent, bool hasClose) : QWidget(parent) -{ +{ _minimizeButton = NULL; _maximizeButton = NULL; _closeButton = NULL; @@ -55,14 +57,14 @@ TitleBar::TitleBar(bool top, QWidget *parent, ITitleParent *titleParent, bool ha _title = NULL; _is_native = false; _titleParent = titleParent; - _is_done_moved = false; + _is_done_moved = false; _is_able_drag = true; assert(parent); setObjectName("TitleBar"); setContentsMargins(0,0,0,0); - setFixedHeight(32); + setFixedHeight(32); QHBoxLayout *lay1 = new QHBoxLayout(this); @@ -76,7 +78,9 @@ TitleBar::TitleBar(bool top, QWidget *parent, ITitleParent *titleParent, bool ha _maximizeButton->setObjectName("MaximizeButton"); lay1->addWidget(_minimizeButton); + lay1->addSpacing(6); lay1->addWidget(_maximizeButton); + lay1->addSpacing(6); connect(this, SIGNAL(normalShow()), parent, SLOT(showNormal())); connect(this, SIGNAL( maximizedShow()), parent, SLOT(showMaximized())); @@ -96,12 +100,12 @@ TitleBar::TitleBar(bool top, QWidget *parent, ITitleParent *titleParent, bool ha lay1->setContentsMargins(0,0,0,0); lay1->setSpacing(0); - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + ADD_UI(this); } -TitleBar::~TitleBar(){ +TitleBar::~TitleBar(){ DESTROY_QT_OBJECT(_minimizeButton); DESTROY_QT_OBJECT(_maximizeButton); DESTROY_QT_OBJECT(_closeButton); @@ -128,14 +132,14 @@ bool TitleBar::ParentIsMaxsized() { if (_titleParent != NULL){ return _titleParent->ParentIsMaxsized(); - } + } else{ return parentWidget()->isMaximized(); } } void TitleBar::paintEvent(QPaintEvent *event) -{ +{ //draw logo icon QStyleOption o; o.initFrom(this); @@ -176,9 +180,9 @@ void TitleBar::setTitle(QString title) } else if (_parent != NULL){ _parent->setWindowTitle(title); - } + } } - + QString TitleBar::title() { if (!_is_native){ @@ -199,7 +203,7 @@ void TitleBar::showMaxRestore() } else { _maximizeButton->setIcon(QIcon(iconPath+"/restore.svg")); maximizedShow(); - } + } } void TitleBar::setRestoreButton(bool max) @@ -211,49 +215,82 @@ void TitleBar::setRestoreButton(bool max) _maximizeButton->setIcon(QIcon(iconPath+"/restore.svg")); } } - + void TitleBar::mousePressEvent(QMouseEvent* event) -{ +{ + // Middle-click-to-minimize is a common native title bar convention on + // Linux desktops (GNOME/KDE/etc.); only the top-level window's bar has a + // minimize button/action. showMinimized() remembers the maximized/normal + // state to restore to, so it needs no extra bookkeeping here. + if (_isTop && event->button() == Qt::MiddleButton) { + _parent->showMinimized(); + event->accept(); + return; + } + bool ableMove = !ParentIsMaxsized(); - if(event->button() == Qt::LeftButton && ableMove && _is_able_drag) + if(event->button() == Qt::LeftButton && ableMove && _is_able_drag) { int x = event->pos().x(); - int y = event->pos().y(); - + int y = event->pos().y(); + bool bTopWidow = AppControl::Instance()->GetTopWindow() == _parent; bool bClick = (x >= 6 && y >= 5 && x <= width() - 6); //top window need resize hit check - + if (!bTopWidow || bClick ){ - _is_draging = true; - _clickPos = event->globalPos(); + // Wayland forbids clients from positioning themselves; manual + // move-by-delta below is a no-op there. Ask the compositor to + // perform the move instead, via the same protocol native + // titlebar dragging uses. + if (QGuiApplication::platformName().startsWith("wayland", Qt::CaseInsensitive)){ + QWindow *win = window()->windowHandle(); + if (win != NULL){ + win->startSystemMove(); + event->accept(); + return; + } + } + + _is_draging = true; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + _clickPos = event->globalPosition().toPoint(); +#else + _clickPos = event->globalPos(); +#endif if (_titleParent != NULL){ _oldPos = _titleParent->GetParentPos(); } else{ - _oldPos = _parent->pos(); + _oldPos = _parent->pos(); } _is_done_moved = false; - + event->accept(); return; - } - } + } + } QWidget::mousePressEvent(event); } void TitleBar::mouseMoveEvent(QMouseEvent *event) -{ - if(_is_draging){ +{ + if(_is_draging){ int datX = 0; int datY = 0; - datX = (event->globalPos().x() - _clickPos.x()); - datY = (event->globalPos().y() - _clickPos.y()); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QPoint globalPos = event->globalPosition().toPoint(); +#else + QPoint globalPos = event->globalPos(); +#endif + datX = (globalPos.x() - _clickPos.x()); + datY = (globalPos.y() - _clickPos.y()); int x = _oldPos.x() + datX; int y = _oldPos.y() + datY; @@ -298,10 +335,10 @@ void TitleBar::mouseMoveEvent(QMouseEvent *event) _parent->move(x, y); } - + event->accept(); return; - } + } QWidget::mouseMoveEvent(event); } @@ -316,10 +353,10 @@ void TitleBar::mouseReleaseEvent(QMouseEvent* event) } void TitleBar::mouseDoubleClickEvent(QMouseEvent *event) -{ - QWidget::mouseDoubleClickEvent(event); +{ + QWidget::mouseDoubleClickEvent(event); - if (_isTop){ + if (_isTop){ QTimer::singleShot(200, this, [this](){ showMaxRestore(); @@ -329,7 +366,7 @@ void TitleBar::mouseDoubleClickEvent(QMouseEvent *event) void TitleBar::UpdateLanguage() { - + } void TitleBar::UpdateTheme() @@ -338,16 +375,37 @@ void TitleBar::UpdateTheme() } void TitleBar::UpdateFont() -{ +{ QFont font = this->font(); font.setPointSizeF(AppConfig::Instance().appOptions.fontSize+1); _title->setFont(font); + + // Scale the bar height with the configured font instead of a bare fixed + // pixel value, so it doesn't look undersized next to a native title bar + // when the font size (or a HiDPI/Wayland text scale) is larger than the + // default. The +14 padding leaves comfortable room for the min/max/close + // button icons; 32 is kept as the floor so the default look is unchanged. + const int textHeight = QFontMetrics(font).height(); + const int barHeight = qMax(32, textHeight + 14); + setFixedHeight(barHeight); + + // Scale the min/max/close button icons (and with them, the buttons' + // clickable area) with the bar height too - the previous default icon + // size (~16px, whatever QToolButton falls back to unset) left them + // looking small and cramped next to a taller, native-sized bar. + const QSize iconSize(barHeight * 0.55, barHeight * 0.55); + if (_minimizeButton != NULL) + _minimizeButton->setIconSize(iconSize); + if (_maximizeButton != NULL) + _maximizeButton->setIconSize(iconSize); + if (_closeButton != NULL) + _closeButton->setIconSize(iconSize); } void TitleBar::EnableAbleDrag(bool bEnabled) { _is_able_drag = bEnabled; } - + } // namespace toolbars } // namespace pv diff --git a/DSView/pv/toolbars/trigbar.cpp b/DSView/pv/toolbars/trigbar.cpp index f79793ffd..288d129ba 100644 --- a/DSView/pv/toolbars/trigbar.cpp +++ b/DSView/pv/toolbars/trigbar.cpp @@ -3,6 +3,7 @@ * DSView is based on PulseView. * * Copyright (C) 2013 DreamSourceLab + * Copyright (C) 2026 Schildkroet * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +30,9 @@ #include "../dialogs/fftoptions.h" #include "../dialogs/lissajousoptions.h" #include "../dialogs/mathoptions.h" +#include "../dialogs/dsohistogram.h" +#include "../dialogs/chanmeasure.h" +#include "../dialogs/refoptions.h" #include "../view/trace.h" #include "../dialogs/applicationpardlg.h" #include "../ui/langresource.h" @@ -47,7 +51,8 @@ TrigBar::TrigBar(SigSession *session, QWidget *parent) : _measure_button(this), _search_button(this), _function_button(this), - _setting_button(this) + _setting_button(this), + _dso_split_button(this) { _enable = true; @@ -59,11 +64,23 @@ TrigBar::TrigBar(SigSession *session, QWidget *parent) : _action_math = new QAction(this); _action_math->setObjectName(QString::fromUtf8("actionMath")); - + + _action_histogram = new QAction(this); + _action_histogram->setObjectName(QString::fromUtf8("actionHistogram")); + + _action_chanmeasure = new QAction(this); + _action_chanmeasure->setObjectName(QString::fromUtf8("actionChanMeasure")); + + _action_reference = new QAction(this); + _action_reference->setObjectName(QString::fromUtf8("actionReference")); + _function_menu = new QMenu(this); _function_menu->setContentsMargins(0,0,0,0); _function_menu->addAction(_action_fft); _function_menu->addAction(_action_math); + _function_menu->addAction(_action_histogram); + _function_menu->addAction(_action_chanmeasure); + _function_menu->addAction(_action_reference); _function_button.setPopupMode(QToolButton::InstantPopup); _function_button.setMenu(_function_menu); @@ -72,14 +89,22 @@ TrigBar::TrigBar(SigSession *session, QWidget *parent) : _dark_style = new QAction(this); _dark_style->setObjectName(QString::fromUtf8("actionDark")); - + _light_style = new QAction(this); _light_style->setObjectName(QString::fromUtf8("actionLight")); - + + _latte_style = new QAction(this); + _latte_style->setObjectName(QString::fromUtf8("actionLatte")); + + _frappe_style = new QAction(this); + _frappe_style->setObjectName(QString::fromUtf8("actionFrappe")); + _themes = new QMenu(this); _themes->setObjectName(QString::fromUtf8("menuThemes")); _themes->addAction(_light_style); _themes->addAction(_dark_style); + _themes->addAction(_latte_style); + _themes->addAction(_frappe_style); _action_dispalyOptions = new QAction(this); @@ -99,6 +124,8 @@ TrigBar::TrigBar(SigSession *session, QWidget *parent) : _search_button.setToolButtonStyle(Qt::ToolButtonTextUnderIcon); _function_button.setToolButtonStyle(Qt::ToolButtonTextUnderIcon); _setting_button.setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + _dso_split_button.setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + _dso_split_button.setCheckable(true); _protocol_button.setContentsMargins(0,0,0,0); @@ -106,19 +133,26 @@ TrigBar::TrigBar(SigSession *session, QWidget *parent) : _protocol_action = addWidget(&_protocol_button); _measure_action = addWidget(&_measure_button); _search_action = addWidget(&_search_button); - _function_action = addWidget(&_function_button); + _function_action = addWidget(&_function_button); _display_action = addWidget(&_setting_button); //must be created + _dso_split_action = addWidget(&_dso_split_button); connect(&_trig_button, SIGNAL(clicked()),this, SLOT(trigger_clicked())); connect(&_protocol_button, SIGNAL(clicked()),this, SLOT(protocol_clicked())); connect(&_measure_button, SIGNAL(clicked()),this, SLOT(measure_clicked())); connect(&_search_button, SIGNAL(clicked()), this, SLOT(search_clicked())); + connect(&_dso_split_button, SIGNAL(clicked()), this, SLOT(dso_split_clicked())); connect(_action_fft, SIGNAL(triggered()), this, SLOT(on_actionFft_triggered())); connect(_action_math, SIGNAL(triggered()), this, SLOT(on_actionMath_triggered())); + connect(_action_histogram, SIGNAL(triggered()), this, SLOT(on_actionHistogram_triggered())); + connect(_action_chanmeasure, SIGNAL(triggered()), this, SLOT(on_actionChanMeasure_triggered())); + connect(_action_reference, SIGNAL(triggered()), this, SLOT(on_actionReference_triggered())); connect(_action_lissajous, SIGNAL(triggered()), this, SLOT(on_actionLissajous_triggered())); connect(_dark_style, SIGNAL(triggered()), this, SLOT(on_actionDark_triggered())); connect(_light_style, SIGNAL(triggered()), this, SLOT(on_actionLight_triggered())); + connect(_latte_style, SIGNAL(triggered()), this, SLOT(on_actionLatte_triggered())); + connect(_frappe_style, SIGNAL(triggered()), this, SLOT(on_actionFrappe_triggered())); connect(_action_dispalyOptions, SIGNAL(triggered()), this, SLOT(on_display_setting())); ADD_UI(this); @@ -136,17 +170,23 @@ void TrigBar::retranslateUi() _measure_button.setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_MEASURE), "Measure")); _search_button.setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_SEARCH), "Search")); _function_button.setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_FUNCTION), "Function")); + _dso_split_button.setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_DSO_SPLIT), "Split")); - _setting_button.setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_DISPLAY), "Display")); + _setting_button.setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_DISPLAY), "Display")); _themes->setTitle(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_DISPLAY_THEMES), "Themes")); _action_lissajous->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_DISPLAY_LISSAJOUS), "Lissajous")); _dark_style->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_DISPLAY_THEMES_DARK), "Dark")); _light_style->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_DISPLAY_THEMES_LIGHT), "Light")); + _latte_style->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_DISPLAY_THEMES_LATTE), "Latte")); + _frappe_style->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_DISPLAY_THEMES_FRAPPE), "Frappé")); _action_fft->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_FUNCTION_FFT), "FFT")); _action_math->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_FUNCTION_MATH), "Math")); + _action_histogram->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_FUNCTION_HISTOGRAM), "Histogram")); + _action_chanmeasure->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_FUNCTION_CHMEASURE), "Ch-Ch Measure")); + _action_reference->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_FUNCTION_REFERENCE), "Reference")); _action_dispalyOptions->setText(L_S(STR_PAGE_TOOLBAR, S_ID(IDS_TOOLBAR_DISPLAY_OPTIONS), "Options")); } @@ -161,17 +201,28 @@ void TrigBar::reStyle() _search_button.setIcon(QIcon(iconPath+"/search-bar.svg")); _function_button.setIcon(QIcon(iconPath+"/function.svg")); _setting_button.setIcon(QIcon(iconPath+"/display.svg")); + _dso_split_button.setIcon(QIcon(iconPath+"/dso-split.svg")); _action_fft->setIcon(QIcon(iconPath+"/fft.svg")); _action_math->setIcon(QIcon(iconPath+"/math.svg")); + _action_histogram->setIcon(QIcon(iconPath+"/measure.svg")); + _action_chanmeasure->setIcon(QIcon(iconPath+"/measure.svg")); + _action_reference->setIcon(QIcon(iconPath+"/math.svg")); _action_lissajous->setIcon(QIcon(iconPath+"/lissajous.svg")); _dark_style->setIcon(QIcon(iconPath+"/dark.svg")); _light_style->setIcon(QIcon(iconPath+"/light.svg")); + // Latte/Frappe have no dedicated glyph asset - reuse whichever of the + // light/dark menu icons matches their background darkness. + _latte_style->setIcon(QIcon(iconPath+"/light.svg")); + _frappe_style->setIcon(QIcon(iconPath+"/dark.svg")); _action_dispalyOptions->setIcon(QIcon(iconPath+"/gear.svg")); AppConfig &app = AppConfig::Instance(); - QString icon_fname = iconPath +"/"+ app.frameOptions.style +".svg"; + // The Themes menu's own icon: fall back to the dark/light glyph for + // color schemes (Latte/Frappe) that don't have a same-named icon file. + QString icon_style = app.IsDarkStyle() ? THEME_STYLE_DARK : THEME_STYLE_LIGHT; + QString icon_fname = iconPath +"/"+ icon_style +".svg"; _themes->setIcon(QIcon(icon_fname)); } @@ -227,6 +278,14 @@ void TrigBar::search_clicked() } } +void TrigBar::dso_split_clicked() +{ + if (_dso_split_button.isVisible() && _dso_split_button.isEnabled()) + { + sig_dso_split(_dso_split_button.isChecked()); + } +} + void TrigBar::reload() { int mode = _session->get_device()->get_work_mode(); @@ -239,6 +298,7 @@ void TrigBar::reload() _function_action->setVisible(false); _action_lissajous->setVisible(false); _action_dispalyOptions->setVisible(true); + _dso_split_action->setVisible(false); } else if (mode == ANALOG) { _trig_action->setVisible(false); @@ -248,6 +308,7 @@ void TrigBar::reload() _function_action->setVisible(false); _action_lissajous->setVisible(false); _action_dispalyOptions->setVisible(true); + _dso_split_action->setVisible(false); } else if (mode == DSO) { _trig_action->setVisible(true); @@ -257,6 +318,8 @@ void TrigBar::reload() _function_action->setVisible(true); _action_lissajous->setVisible(true); _action_dispalyOptions->setVisible(true); + _dso_split_action->setVisible(true); + _dso_split_button.setChecked(AppConfig::Instance().appOptions.dsoSplitChannels); } DockOptions *opt = getDockOptions(); @@ -291,6 +354,24 @@ void TrigBar::on_actionMath_triggered() } } +void TrigBar::on_actionHistogram_triggered() +{ + pv::dialogs::DsoHistogram hist_dlg(_session, this); + hist_dlg.exec(); +} + +void TrigBar::on_actionChanMeasure_triggered() +{ + pv::dialogs::DsoChannelMeasure ch_dlg(_session, this); + ch_dlg.exec(); +} + +void TrigBar::on_actionReference_triggered() +{ + pv::dialogs::RefOptions ref_dlg(_session, this); + ref_dlg.exec(); +} + void TrigBar::on_actionDark_triggered() { sig_setTheme(THEME_STYLE_DARK); @@ -305,6 +386,24 @@ void TrigBar::on_actionLight_triggered() _themes->setIcon(QIcon(icon)); } +void TrigBar::on_actionLatte_triggered() +{ + sig_setTheme(THEME_STYLE_LATTE); + // Latte has no dedicated glyph asset - it reuses the "light" icon set, + // so use that icon set's "light.svg" glyph for the menu icon too. + QString icon = GetIconPath() + "/" + THEME_STYLE_LIGHT + ".svg"; + _themes->setIcon(QIcon(icon)); +} + +void TrigBar::on_actionFrappe_triggered() +{ + sig_setTheme(THEME_STYLE_FRAPPE); + // Frappe has no dedicated glyph asset - it reuses the "dark" icon set, + // so use that icon set's "dark.svg" glyph for the menu icon too. + QString icon = GetIconPath() + "/" + THEME_STYLE_DARK + ".svg"; + _themes->setIcon(QIcon(icon)); +} + void TrigBar::on_actionLissajous_triggered() { pv::dialogs::LissajousOptions lissajous_dlg(_session, this); diff --git a/DSView/pv/toolbars/trigbar.h b/DSView/pv/toolbars/trigbar.h index 95d18380f..024f432d7 100644 --- a/DSView/pv/toolbars/trigbar.h +++ b/DSView/pv/toolbars/trigbar.h @@ -3,6 +3,7 @@ * DSView is based on PulseView. * * Copyright (C) 2013 DreamSourceLab + * Copyright (C) 2026 Schildkroet * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -68,13 +69,19 @@ class TrigBar : public QToolBar, public IUiWindow void sig_measure(bool visible);//post decode button click event,to show or hide measure property panel void sig_search(bool visible); void sig_show_lissajous(bool visible); + void sig_dso_split(bool split); //split each DSO channel into its own row private slots: void on_actionDark_triggered(); void on_actionLight_triggered(); + void on_actionLatte_triggered(); + void on_actionFrappe_triggered(); void on_actionLissajous_triggered(); void on_actionFft_triggered(); void on_actionMath_triggered(); + void on_actionHistogram_triggered(); + void on_actionChanMeasure_triggered(); + void on_actionReference_triggered(); void on_display_setting(); public slots: @@ -82,6 +89,7 @@ public slots: void trigger_clicked(); void measure_clicked(); void search_clicked(); + void dso_split_clicked(); private: SigSession *_session; @@ -92,22 +100,29 @@ public slots: XToolButton _search_button; XToolButton _function_button; XToolButton _setting_button; + XToolButton _dso_split_button; QAction *_trig_action; QAction *_protocol_action; QAction *_measure_action; QAction *_search_action; - QAction *_function_action; + QAction *_function_action; QAction *_display_action; + QAction *_dso_split_action; QMenu *_function_menu; QAction *_action_fft; QAction *_action_math; + QAction *_action_histogram; + QAction *_action_chanmeasure; + QAction *_action_reference; QMenu *_display_menu; QMenu *_themes; QAction *_action_dispalyOptions; QAction *_dark_style; QAction *_light_style; + QAction *_latte_style; + QAction *_frappe_style; QAction *_action_lissajous; }; diff --git a/DSView/pv/ui/langresource.h b/DSView/pv/ui/langresource.h index f8e84cd95..9755752a6 100644 --- a/DSView/pv/ui/langresource.h +++ b/DSView/pv/ui/langresource.h @@ -58,10 +58,11 @@ struct lang_page_item bool is_dynamic; }; -static const struct lang_key_item lang_id_keys[] = +static const struct lang_key_item lang_id_keys[] = { {25, "cn"}, - {31, "en"} + {31, "en"}, + {7, "de"} }; static const struct lang_page_item lange_page_keys[] = @@ -89,7 +90,7 @@ class LangResource void release_dynamic(); inline bool is_lang_en(){ - return _cur_lang == 31; + return _cur_lang == 31 || _cur_lang == 7; } private: diff --git a/DSView/pv/view/cursor.cpp b/DSView/pv/view/cursor.cpp index 1c3e707d1..eec060188 100644 --- a/DSView/pv/view/cursor.cpp +++ b/DSView/pv/view/cursor.cpp @@ -49,7 +49,7 @@ const int Cursor::CloseSize = 10; Cursor::Cursor(View &view, int order, uint64_t sampleIndex) : TimeMarker(view, sampleIndex) { - _order = _order; + _order = order; } QRect Cursor::get_label_rect(const QRect &rect, bool &visible, bool has_hoff) diff --git a/DSView/pv/view/decodetrace.cpp b/DSView/pv/view/decodetrace.cpp index e79c0b2df..e9e48785a 100644 --- a/DSView/pv/view/decodetrace.cpp +++ b/DSView/pv/view/decodetrace.cpp @@ -111,6 +111,54 @@ const QColor DecodeTrace::OutlineColours[16] = { QColor(0x6B, 0x23, 0x37) }; +// Official Catppuccin Frappe accent colours +const QColor DecodeTrace::CatppuccinFrappeColours[14] = { + QColor(0xe7, 0x82, 0x84), // Red + QColor(0xea, 0x99, 0x9c), // Maroon + QColor(0xef, 0x9f, 0x76), // Peach + QColor(0xe5, 0xc8, 0x90), // Yellow + QColor(0xa6, 0xd1, 0x89), // Green + QColor(0x81, 0xc8, 0xbe), // Teal + QColor(0x99, 0xd1, 0xdb), // Sky + QColor(0x85, 0xc1, 0xdc), // Sapphire + QColor(0x8c, 0xaa, 0xee), // Blue + QColor(0xba, 0xbb, 0xf1), // Lavender + QColor(0xca, 0x9e, 0xe6), // Mauve + QColor(0xf4, 0xb8, 0xe4), // Pink + QColor(0xee, 0xbe, 0xbe), // Flamingo + QColor(0xf2, 0xd5, 0xcf) // Rosewater +}; + +// Official Catppuccin Latte accent colours +const QColor DecodeTrace::CatppuccinLatteColours[14] = { + QColor(0xd2, 0x0f, 0x39), // Red + QColor(0xe6, 0x45, 0x53), // Maroon + QColor(0xfe, 0x64, 0x0b), // Peach + QColor(0xdf, 0x8e, 0x1d), // Yellow + QColor(0x40, 0xa0, 0x2b), // Green + QColor(0x17, 0x92, 0x99), // Teal + QColor(0x04, 0xa5, 0xe5), // Sky + QColor(0x20, 0x9f, 0xb5), // Sapphire + QColor(0x1e, 0x66, 0xf5), // Blue + QColor(0x72, 0x87, 0xfd), // Lavender + QColor(0x88, 0x39, 0xef), // Mauve + QColor(0xea, 0x76, 0xcb), // Pink + QColor(0xdd, 0x78, 0x78), // Flamingo + QColor(0xdc, 0x8a, 0x78) // Rosewater +}; + +QColor DecodeTrace::get_row_base_colour(int local_row) +{ + const QString &style = AppConfig::Instance().frameOptions.style; + + if (style == THEME_STYLE_FRAPPE) + return CatppuccinFrappeColours[local_row % countof(CatppuccinFrappeColours)]; + else if (style == THEME_STYLE_LATTE) + return CatppuccinLatteColours[local_row % countof(CatppuccinLatteColours)]; + + return Colours[local_row % countof(Colours)]; +} + DecodeTrace::DecodeTrace(pv::SigSession *session, pv::data::DecoderStack *decoder_stack, int index) : @@ -365,7 +413,8 @@ void DecodeTrace::draw_annotation(const pv::data::decode::Annotation &a, const double end = min(a.end_sample() / samples_per_pixel - pixels_offset, (double)right); - const QColor &fill_base = _colour.isValid() ? _colour : fore; + const QColor fill_base = _colour.isValid() ? _colour : + get_row_base_colour(local_row); QColor fill, outline; generate_annotation_colours(fill_base, local_row, a, &fill, &outline); const QColor &text_color = get_text_colour(fill); @@ -487,6 +536,7 @@ void DecodeTrace::draw_range(const pv::data::decode::Annotation &a, QPainter &p, double end, int y, QColor fore, QColor back) { (void)fore; + (void)back; AppConfig &app = AppConfig::Instance(); bool fontStretch = app.appOptions.decoderDynamicFontWidth; @@ -531,9 +581,7 @@ void DecodeTrace::draw_range(const pv::data::decode::Annotation &a, QPainter &p, // Try to find an annotation that will fit QString best_annotation; - int best_width = 0; int max_width = rect.width(); - bool isCondensed = false; QFontMetrics fm = p.fontMetrics(); QFont condensed_font = p.font(); double MIN_STRETCH = fontStretch @@ -562,8 +610,6 @@ void DecodeTrace::draw_range(const pv::data::decode::Annotation &a, QPainter &p, if (stretch >= MIN_STRETCH) { // fitting annotation found with condensing best_annotation = a; - best_width = ceil((double)w * (stretch / 100.0)); - isCondensed = true; break; } } diff --git a/DSView/pv/view/decodetrace.h b/DSView/pv/view/decodetrace.h index c521a8f64..2ec549256 100644 --- a/DSView/pv/view/decodetrace.h +++ b/DSView/pv/view/decodetrace.h @@ -79,6 +79,9 @@ class DecodeTrace : public Trace static const QColor Colours[16]; static const QColor OutlineColours[16]; + static const QColor CatppuccinFrappeColours[14]; + static const QColor CatppuccinLatteColours[14]; + static const int ControlRectWidth = 5; static const int MaxAnnType = 100; @@ -172,6 +175,8 @@ class DecodeTrace : public Trace void generate_annotation_colours(QColor baseColour, int local_row, const pv::data::decode::Annotation& a, QColor *fill, QColor *outline); + static QColor get_row_base_colour(int local_row); + signals: void decoded_progress(int progress); diff --git a/DSView/pv/view/dsldial.cpp b/DSView/pv/view/dsldial.cpp index 0453b3e08..aefb9bd02 100644 --- a/DSView/pv/view/dsldial.cpp +++ b/DSView/pv/view/dsldial.cpp @@ -99,7 +99,7 @@ void dslDial::paint(QPainter &p, QRectF dialRect, QColor dialColor, const QPoint displayIndex++; } - assert(displayIndex < _unit.count()); + assert((qsizetype)displayIndex < _unit.count()); pText = QString::number(displayValue) + _unit[displayIndex] + "/div"; diff --git a/DSView/pv/view/dsosignal.cpp b/DSView/pv/view/dsosignal.cpp index 7b700f8d9..b1fbc9a2e 100644 --- a/DSView/pv/view/dsosignal.cpp +++ b/DSView/pv/view/dsosignal.cpp @@ -3,6 +3,7 @@ * DSView is based on PulseView. * * Copyright (C) 2013 DreamSourceLab + * Copyright (C) 2026 Schildkroet * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -28,7 +29,8 @@ #include "view.h" #include "../dsvdef.h" #include "../data/dsosnapshot.h" -#include "../sigsession.h" +#include "../data/dsoedgedetect.h" +#include "../sigsession.h" #include "../log.h" #include "../appcontrol.h" #include "../ui/langresource.h" @@ -75,6 +77,10 @@ DsoSignal::DsoSignal(data::DsoSnapshot *data, _vDialActive = false; _mValid = false; _level_valid = false; + _soft_measure_logged = false; + _soft_measure_cache_valid = false; + _soft_measure_cache_data = NULL; + _soft_measure_cache_sample_count = 0; _autoV = false; _autoH = false; _autoV_over = false; @@ -640,6 +646,15 @@ QString DsoSignal::get_measure(enum DSO_MEASURE_TYPE type) QRect DsoSignal::get_view_rect() { assert(_viewport); + + if (_view && _view->get_dso_split_channels()){ + const int top = get_v_offset() - get_totalHeight() / 2; + const int height = max(get_totalHeight() - UpMargin - DownMargin, 1); + return QRect(0, top + UpMargin, + _viewport->width() - RightMargin, + height); + } + return QRect(0, UpMargin, _viewport->width() - RightMargin, _viewport->height() - UpMargin - DownMargin); @@ -697,7 +712,12 @@ void DsoSignal::paint_back(QPainter &p, int left, int right, QColor fore, QColor int i, j; const int height = get_view_rect().height(); - const int width = right - left; + const int width = right - left; + // Origin of this channel's own band. In the default (overlaid) mode this + // is just UpMargin, same as before; in split mode each channel has its + // own row, so the grid must be anchored to that row instead of the + // viewport's absolute top. + const int top = get_view_rect().top(); fore.setAlpha(View::BackAlpha); @@ -705,7 +725,7 @@ void DsoSignal::paint_back(QPainter &p, int left, int right, QColor fore, QColor solidPen.setStyle(Qt::SolidLine); p.setPen(solidPen); p.setBrush(back.black() > 0x80 ? back.darker() : back.lighter()); - p.drawRect(left, UpMargin, width, height); + p.drawRect(left, top, width, height); // draw zoom region fore.setAlpha(View::ForeAlpha); @@ -718,20 +738,21 @@ void DsoSignal::paint_back(QPainter &p, int left, int right, QColor fore, QColor const double start = _view->x_offset() * samples_per_pixel; const double shown_offset = min(start / sample_len, 1.0) * width; const double shown_len = max(shown_rate * width, 6.0); - const QPointF left_edge[] = {QPoint(shown_offset + 3, UpMargin/2 - 6), - QPoint(shown_offset, UpMargin/2 - 6), - QPoint(shown_offset, UpMargin/2 + 6), - QPoint(shown_offset + 3, UpMargin/2 + 6)}; - const QPointF right_edge[] = {QPoint(shown_offset + shown_len - 3, UpMargin/2 - 6), - QPoint(shown_offset + shown_len , UpMargin/2 - 6), - QPoint(shown_offset + shown_len , UpMargin/2 + 6), - QPoint(shown_offset + shown_len - 3, UpMargin/2 + 6)}; - p.drawLine(left, UpMargin/2, shown_offset, UpMargin/2); - p.drawLine(shown_offset + shown_len, UpMargin/2, left + width, UpMargin/2); + const double markerY = top - UpMargin / 2; + const QPointF left_edge[] = {QPoint(shown_offset + 3, markerY - 6), + QPoint(shown_offset, markerY - 6), + QPoint(shown_offset, markerY + 6), + QPoint(shown_offset + 3, markerY + 6)}; + const QPointF right_edge[] = {QPoint(shown_offset + shown_len - 3, markerY - 6), + QPoint(shown_offset + shown_len , markerY - 6), + QPoint(shown_offset + shown_len , markerY + 6), + QPoint(shown_offset + shown_len - 3, markerY + 6)}; + p.drawLine(left, markerY, shown_offset, markerY); + p.drawLine(shown_offset + shown_len, markerY, left + width, markerY); p.drawPolyline(left_edge, countof(left_edge)); p.drawPolyline(right_edge, countof(right_edge)); p.setBrush(fore); - p.drawRect(shown_offset, UpMargin/2 - 3, shown_len, 6); + p.drawRect(shown_offset, markerY - 3, shown_len, 6); // draw divider fore.setAlpha(View::BackAlpha); @@ -740,7 +761,7 @@ void DsoSignal::paint_back(QPainter &p, int left, int right, QColor fore, QColor p.setPen(dashPen); const double spanY =height * 1.0 / DS_CONF_DSO_VDIVS; for (i = 1; i <= DS_CONF_DSO_VDIVS; i++) { - const double posY = spanY * i + UpMargin; + const double posY = spanY * i + top; if (i != DS_CONF_DSO_VDIVS) p.drawLine(left, posY, right, posY); const double miniSpanY = spanY / 5; @@ -753,14 +774,20 @@ void DsoSignal::paint_back(QPainter &p, int left, int right, QColor fore, QColor for (i = 1; i <= DS_CONF_DSO_HDIVS; i++) { const double posX = spanX * i; if (i != DS_CONF_DSO_HDIVS) - p.drawLine(posX, UpMargin,posX, height + UpMargin); + p.drawLine(posX, top, posX, height + top); const double miniSpanX = spanX / 5; for (j = 1; j < 5; j++) { - p.drawLine(posX - miniSpanX * j, height / 2.0f + UpMargin - 5, - posX - miniSpanX * j, height / 2.0f + UpMargin + 5); + p.drawLine(posX - miniSpanX * j, height / 2.0f + top - 5, + posX - miniSpanX * j, height / 2.0f + top + 5); } } - _view->set_back(true); + + // In overlaid mode every DSO channel shares the same background, so + // painting it once is enough (see Viewport::doPaint()'s back_ready() + // short-circuit). In split mode each channel has its own band and must + // paint its own background. + if (!_view->get_dso_split_channels()) + _view->set_back(true); } void DsoSignal::paint_mid(QPainter &p, int left, int right, QColor fore, QColor back) @@ -864,6 +891,44 @@ void DsoSignal::paint_mid(QPainter &p, int left, int right, QColor fore, QColor _mean = (index == 0) ? status.ch0_acc_mean : status.ch1_acc_mean; _mean = hw_offset - _mean / _data->get_sample_count(); } + + // The hardware's own cycle/level measurement engine can fail to + // evaluate a channel correctly - most commonly because that + // channel's trigger is misconfigured (the trigger comparator and + // the auto-measurement level detection share the same hardware + // path), while the displayed waveform is unaffected since it is + // just the raw streamed samples. Fall back to a software + // measurement derived from those samples in that case. + if (!_level_valid || _period == 0) { + if (!_soft_measure_logged) { + // Log the raw hardware fields behind the fallback decision + // (not just the fact of it), so it is possible to tell + // whether the device genuinely found no valid cycle + // (count/levels read 0) or reports invalid despite having + // usable-looking counters. + const uint32_t count = (index == 0) ? status.ch0_cyc_cnt : status.ch1_cyc_cnt; + const uint32_t tlen = (index == 0) ? status.ch0_cyc_tlen : status.ch1_cyc_tlen; + const uint8_t lvl_high = (index == 0) ? status.ch0_high_level : status.ch1_high_level; + const uint8_t lvl_low = (index == 0) ? status.ch0_low_level : status.ch1_low_level; + dsv_info("DsoSignal: channel %d reports no valid hardware " + "cycle measurement (level_valid=%d, cyc_cnt=%u, " + "cyc_tlen=%u, high_level=%u, low_level=%u, " + "max=%u, min=%u), using software fallback. " + "This channel's trigger is probably set wrong " + "(the hardware measurement engine shares the " + "trigger comparator, so a misconfigured trigger " + "can break measurement without affecting the " + "displayed waveform).", + index, _level_valid, count, tlen, + lvl_high, lvl_low, _max, _min); + _soft_measure_logged = true; + } + compute_soft_measure(hw_offset); + } + else { + // Hardware recovered: log again if it drops out later. + _soft_measure_logged = false; + } } } } @@ -1014,6 +1079,111 @@ void DsoSignal::paint_trace(QPainter &p, } } +void DsoSignal::compute_soft_measure(int hw_offset) +{ + if (_data == NULL || _data->empty()) + return; + + const uint64_t total = _data->get_sample_count(); + if (total < 2) + return; + + // paint_mid() calls this on every repaint for as long as the hardware + // measurement stays invalid, which - while an acquisition is actively + // running - can be many times per second. Skip the O(n) rescan below + // unless the underlying dataset actually changed. get_trig_time() is + // used rather than just the sample count because the configured capture + // depth (and so get_sample_count()) is normally the same on every run, + // which would otherwise make this cache never invalidate across repeat + // captures. + const QDateTime trig_time = session->get_trig_time(); + if (_soft_measure_cache_valid && + _soft_measure_cache_data == _data && + _soft_measure_cache_sample_count == total && + _soft_measure_cache_trig_time == trig_time) { + return; // _period/_high_time/etc already hold the current result + } + + const uint8_t *buf = _data->get_samples(0, 0, get_index()); + if (buf == NULL) + return; + + const double samplerate = _data->samplerate(); + if (samplerate <= 0) + return; + + uint16_t total_channels = g_slist_length(session->get_device()->get_channels()); + if (total_channels == 1 && _data->is_file()) + total_channels++; + const uint16_t enabled_channels = _data->get_channel_num(); + if (enabled_channels == 0) + return; + + // Nanoseconds per channel sample (matches the hardware measurement path). + const double tfactor = ((double)total_channels / enabled_channels) + * SR_GHZ(1) * 1.0 / samplerate; + + // Cap the scan so continuous repaints stay cheap; this still covers many + // cycles for a stable measurement. + const uint64_t MaxScan = 1000000; + const uint64_t n = min(total, MaxScan); + + // Once we reach here, we are about to (re)compute the result for this + // dataset - remember its identity so the next call can skip straight to + // the early-return above until the data changes again. + _soft_measure_cache_valid = true; + _soft_measure_cache_data = _data; + _soft_measure_cache_sample_count = total; + _soft_measure_cache_trig_time = trig_time; + + // Work in voltage-proportional space (higher value = higher voltage) so + // "high time" matches the hardware's positive-duty convention. + auto val = [&](uint64_t i) -> double { return (double)hw_offset - buf[i]; }; + + int rmin = 255, rmax = 0; + for (uint64_t i = 0; i < n; i++) { + rmin = min(rmin, (int)buf[i]); + rmax = max(rmax, (int)buf[i]); + } + + const data::DsoEdgeSet edge_set = data::dso_detect_edges(n, val); + const std::vector &rising = edge_set.rising; + const std::vector &falling = edge_set.falling; + + if (rising.size() < 2) + return; // flat, or not enough cycles + + // Mean period (samples) from consecutive rising edges. + double psum = 0; + for (size_t k = 1; k < rising.size(); k++) + psum += rising[k] - rising[k - 1]; + const double period_samples = psum / (rising.size() - 1); + + // Mean high-level duration: rising edge to the next falling edge. + double hsum = 0; + int hn = 0; + size_t fi = 0; + for (size_t k = 0; k < rising.size(); k++) { + while (fi < falling.size() && falling[fi] <= rising[k]) + fi++; + if (fi < falling.size()) { + hsum += (double)(falling[fi] - rising[k]); + hn++; + } + } + const double high_samples = (hn > 0) ? hsum / hn : 0.0; + + _period = period_samples * tfactor; + _high_time = high_samples * tfactor; + _pcount = (uint32_t)rising.size(); + _min = (uint8_t)rmin; + _max = (uint8_t)rmax; + _low = (uint8_t)rmin; + _high = (uint8_t)rmax; + _level_valid = true; + _mValid = true; +} + void DsoSignal::paint_envelope(QPainter &p, const pv::data::DsoSnapshot *snapshot, int zeroY, int left, const int64_t start, const int64_t end, int hw_offset, @@ -1220,6 +1390,9 @@ QRectF DsoSignal::get_rect(DsoSetRegions type, int y, int right) { (void)right; + const int SquareWidth = get_squareWidth(); + const int Margin = get_squareMargin(); + if (type == DSO_VDIAL) return QRectF( get_leftWidth() + SquareWidth*0.5 + Margin, @@ -1262,25 +1435,15 @@ QRectF DsoSignal::get_rect(DsoSetRegions type, int y, int right) void DsoSignal::paint_hover_measure(QPainter &p, QColor fore, QColor back) { const int hw_offset = get_hw_offset(); - // Hover measure - if (_hover_en && _hover_point != QPointF(-1, -1)) { - QString hover_str = get_voltage(hw_offset - _hover_value, 2); - const int hover_width = p.boundingRect(0, 0, INT_MAX, INT_MAX, - Qt::AlignLeft | Qt::AlignTop, hover_str).width() + 10; - const int hover_height = p.boundingRect(0, 0, INT_MAX, INT_MAX, - Qt::AlignLeft | Qt::AlignTop, hover_str).height(); - QRectF hover_rect(_hover_point.x(), _hover_point.y()-hover_height/2, hover_width, hover_height); - if (hover_rect.right() > get_view_rect().right()) - hover_rect.moveRight(_hover_point.x()); - if (hover_rect.top() < get_view_rect().top()) - hover_rect.moveTop(_hover_point.y()); - if (hover_rect.bottom() > get_view_rect().bottom()) - hover_rect.moveBottom(_hover_point.y()); + // Hover measure. Only the point marker is drawn on the trace here; the + // voltage value itself is shown in the consolidated floating panel drawn + // by Viewport::paintMeasure (see the DSO_VALUE branch), which is far + // easier to read than a number printed on top of the waveform. + if (_hover_en && _hover_point != QPointF(-1, -1)) { p.setPen(fore); p.setBrush(back); p.drawRect(_hover_point.x()-1, _hover_point.y()-1, HoverPointSize, HoverPointSize); - p.drawText(hover_rect, Qt::AlignCenter | Qt::AlignTop | Qt::TextDontClip, hover_str); } auto &cursor_list = _view->get_cursorList(); diff --git a/DSView/pv/view/dsosignal.h b/DSView/pv/view/dsosignal.h index 660c9bb7e..b109d2cea 100644 --- a/DSView/pv/view/dsosignal.h +++ b/DSView/pv/view/dsosignal.h @@ -3,6 +3,7 @@ * DSView is based on PulseView. * * Copyright (C) 2013 DreamSourceLab + * Copyright (C) 2026 Schildkroet * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,6 +26,7 @@ #include "signal.h" #include "../dstimer.h" +#include namespace pv { namespace data { @@ -260,6 +262,11 @@ class DsoSignal : public Signal uint64_t num_channels); void paint_hover_measure(QPainter &p, QColor fore, QColor back); + + // Compute the cycle measurements (period/frequency/duty/count/width/level) + // from the sample buffer. Used as a fallback for channels the hardware + // leaves unmeasured - e.g. the 2nd channel often returns no cycle data. + void compute_soft_measure(int hw_offset); void auto_set(); void call_auto_end(); @@ -287,6 +294,22 @@ class DsoSignal : public Signal uint8_t _min; double _period; bool _level_valid; + // Set once compute_soft_measure() has logged that it is substituting for + // missing hardware cycle data, so repeated repaints don't spam the log. + // Cleared again once the hardware reports valid cycle data. + bool _soft_measure_logged; + + // Identifies the dataset compute_soft_measure() last computed its result + // for, so it can skip re-scanning the sample buffer on repaints that + // don't follow a new acquisition (e.g. hover/cursor redraws while the + // hardware measurement stays invalid for many consecutive frames). + // get_trig_time() changes on every new capture even when the configured + // sample depth (and so get_sample_count()) stays the same between runs, + // which a count-only check would miss. + bool _soft_measure_cache_valid; + const pv::data::DsoSnapshot *_soft_measure_cache_data; + uint64_t _soft_measure_cache_sample_count; + QDateTime _soft_measure_cache_trig_time; uint8_t _high; uint8_t _low; double _rms; diff --git a/DSView/pv/view/header.cpp b/DSView/pv/view/header.cpp index 9dce5247b..4d91ac45e 100644 --- a/DSView/pv/view/header.cpp +++ b/DSView/pv/view/header.cpp @@ -135,9 +135,9 @@ void Header::paintEvent(QPaintEvent*) fore.setAlpha(View::ForeAlpha); QFont font(painter.font()); - float fSize = AppConfig::Instance().appOptions.fontSize; - if (fSize > 10) - fSize = 10; + float fSize = AppConfig::Instance().GetTraceFontSize(); + // Grow channel-label text together with the trace height (logic mode). + fSize *= _view.get_trace_font_scale(); font.setPointSizeF(fSize); painter.setFont(font); @@ -389,8 +389,10 @@ void Header::changeName(QMouseEvent *event) { header_resize(); QFont font = this->font(); - float fsize = AppConfig::Instance().appOptions.fontSize; - font.setPointSizeF(fsize <= 10 ? fsize: 10); + float fsize = AppConfig::Instance().GetTraceFontSize(); + // Match the scaled label font used when painting the channel name. + fsize *= _context_trace->get_label_scale(); + font.setPointSizeF(fsize); nameEdit->setFont(font); nameEdit->setText(_context_trace->get_name()); diff --git a/DSView/pv/view/logicsignal.cpp b/DSView/pv/view/logicsignal.cpp index 24eb2f1d2..28aff3992 100644 --- a/DSView/pv/view/logicsignal.cpp +++ b/DSView/pv/view/logicsignal.cpp @@ -23,11 +23,12 @@ #include #include #include "logicsignal.h" -#include "view.h" +#include "view.h" #include "../data/logicsnapshot.h" #include "view.h" #include "../dsvdef.h" #include "../log.h" +#include "../config/appconfig.h" using namespace std; @@ -44,7 +45,7 @@ LogicSignal::LogicSignal(data::LogicSnapshot *data, Signal(probe), _data(data) { - _trig = NONTRIG; + _trig = NONTRIG; _paint_align_sample_count = 0; } @@ -54,7 +55,7 @@ LogicSignal::LogicSignal(view::LogicSignal *s, Signal(*s, probe), _data(data), _trig(s->get_trig()) -{ +{ _paint_align_sample_count = 0; } @@ -64,6 +65,27 @@ LogicSignal::~LogicSignal() _cur_pulses.clear(); } +void LogicSignal::paint_back(QPainter &p, int left, int right, QColor fore, QColor back) +{ + Trace::paint_back(p, left, right, fore, back); + + // Optional per-channel divider. Logic channels have no fill of their own, + // so without an explicit boundary adjacent rows are hard to tell apart; + // draw a divider under each channel's row, in the middle of the gap to the + // next one. Can be disabled to restore the classic borderless look. + if (!AppConfig::Instance().appOptions.logicChannelDivider) + return; + + QColor sep(fore); + sep.setAlpha(180); + QPen pen(sep); + pen.setWidth(2); + p.setPen(pen); + + const int bottom = get_y() + get_totalHeight() / 2 + View::SignalMargin; + p.drawLine(left, bottom, right, bottom); +} + void LogicSignal::set_trig(int trig) { if (trig > NONTRIG && trig <= EDGTRIG) @@ -77,7 +99,7 @@ bool LogicSignal::commit_trig() if (_trig == NONTRIG) { ds_trigger_probe_set(_index_list.front(), 'X', 'X'); return false; - } + } else { ds_trigger_set_en(true); if (_trig == POSTRIG) @@ -118,6 +140,9 @@ void LogicSignal::paint_mid_align(QPainter &p, int left, int right, QColor fore, const int y = get_y() + _totalHeight * 0.5; const double scale = _view->scale(); assert(scale > 0); + if (scale <= 0) + return; + const int64_t offset = _view->x_offset(); const int high_offset = y - _totalHeight + 0.5f; @@ -126,7 +151,7 @@ void LogicSignal::paint_mid_align(QPainter &p, int left, int right, QColor fore, double samplerate = _data->samplerate(); if (_data->empty() || samplerate == 0) return; - + if (!_data->has_data(_probe->index)) return; @@ -141,7 +166,7 @@ void LogicSignal::paint_mid_align(QPainter &p, int left, int right, QColor fore, const double end = (offset + width + 1) * samples_per_pixel; const uint64_t end_index = min(max((int64_t)floor(end), (int64_t)0), last_sample); const uint64_t start_index = max((uint64_t)floor(start), (uint64_t)0); - + if (start_index > end_index) return; @@ -158,7 +183,7 @@ void LogicSignal::paint_mid_align(QPainter &p, int left, int right, QColor fore, int preY = first_sample ? high_offset : low_offset; int x = preX; std::vector wave_lines; - + if (_cur_edges.size() < max_togs) { std::vector>::const_iterator i; for (i = _cur_edges.begin() + 1; i != _cur_edges.end() - 1; i++) { @@ -186,7 +211,9 @@ void LogicSignal::paint_mid_align(QPainter &p, int left, int right, QColor fore, wave_lines.push_back(QLine(preX, preY, x, preY)); } - p.setPen(_colour.isValid() ? _colour : fore); + QColor defaultColour = get_default_colour(); + QColor lineColour = _colour.isValid() ? _colour : (defaultColour.isValid() ? defaultColour : fore); + p.setPen(QPen(lineColour, AppConfig::Instance().appOptions.logicSignalLineWidth)); p.drawLines(wave_lines.data(), wave_lines.size()); } @@ -225,7 +252,7 @@ void LogicSignal::paint_type_options(QPainter &p, int right, const QPoint pt, QC p.setPen(Qt::NoPen); if (true) - { + { QColor color = View::Blue; if (session->is_loop_mode()){ @@ -247,45 +274,51 @@ void LogicSignal::paint_type_options(QPainter &p, int right, const QPoint pt, QC p.setBrush(edgeTrig_rect.contains(pt) ? color.lighter() : (_trig == EDGTRIG) ? color : Qt::transparent); p.drawRect(edgeTrig_rect); - } + } p.setPen(QPen(fore, 1, Qt::DashLine)); p.setBrush(Qt::transparent); p.drawLine(posTrig_rect.left(), posTrig_rect.bottom(), edgeTrig_rect.right(), edgeTrig_rect.bottom()); - p.setPen(QPen(fore, 2, Qt::SolidLine)); + // Scale the glyph insets/stroke so the trigger symbols grow with the box. + const double sc = get_label_scale(); + const double i5 = 5 * sc; + const double i7 = 7 * sc; + const double i2 = 2 * sc; + + p.setPen(QPen(fore, max(2.0, 2 * sc), Qt::SolidLine)); p.setBrush(Qt::transparent); - p.drawLine(posTrig_rect.left() + 5, posTrig_rect.bottom() - 5, - posTrig_rect.center().x(), posTrig_rect.bottom() - 5); - p.drawLine(posTrig_rect.center().x(), posTrig_rect.bottom() - 5, - posTrig_rect.center().x(), posTrig_rect.top() + 5); - p.drawLine(posTrig_rect.center().x(), posTrig_rect.top() + 5, - posTrig_rect.right() - 5, posTrig_rect.top() + 5); - - p.drawLine(higTrig_rect.left() + 5, higTrig_rect.top() + 5, - higTrig_rect.right() - 5, higTrig_rect.top() + 5); - - p.drawLine(negTrig_rect.left() + 5, negTrig_rect.top() + 5, - negTrig_rect.center().x(), negTrig_rect.top() + 5); - p.drawLine(negTrig_rect.center().x(), negTrig_rect.top() + 5, - negTrig_rect.center().x(), negTrig_rect.bottom() - 5); - p.drawLine(negTrig_rect.center().x(), negTrig_rect.bottom() - 5, - negTrig_rect.right() - 5, negTrig_rect.bottom() - 5); - - p.drawLine(lowTrig_rect.left() + 5, lowTrig_rect.bottom() - 5, - lowTrig_rect.right() - 5, lowTrig_rect.bottom() - 5); - - p.drawLine(edgeTrig_rect.left() + 5, edgeTrig_rect.top() + 5, - edgeTrig_rect.center().x() - 2, edgeTrig_rect.top() + 5); - p.drawLine(edgeTrig_rect.center().x() + 2 , edgeTrig_rect.top() + 5, - edgeTrig_rect.right() - 5, edgeTrig_rect.top() + 5); - p.drawLine(edgeTrig_rect.center().x(), edgeTrig_rect.top() + 7, - edgeTrig_rect.center().x(), edgeTrig_rect.bottom() - 7); - p.drawLine(edgeTrig_rect.left() + 5, edgeTrig_rect.bottom() - 5, - edgeTrig_rect.center().x() - 2, edgeTrig_rect.bottom() - 5); - p.drawLine(edgeTrig_rect.center().x() + 2, edgeTrig_rect.bottom() - 5, - edgeTrig_rect.right() - 5, edgeTrig_rect.bottom() - 5); + p.drawLine(QPointF(posTrig_rect.left() + i5, posTrig_rect.bottom() - i5), + QPointF(posTrig_rect.center().x(), posTrig_rect.bottom() - i5)); + p.drawLine(QPointF(posTrig_rect.center().x(), posTrig_rect.bottom() - i5), + QPointF(posTrig_rect.center().x(), posTrig_rect.top() + i5)); + p.drawLine(QPointF(posTrig_rect.center().x(), posTrig_rect.top() + i5), + QPointF(posTrig_rect.right() - i5, posTrig_rect.top() + i5)); + + p.drawLine(QPointF(higTrig_rect.left() + i5, higTrig_rect.top() + i5), + QPointF(higTrig_rect.right() - i5, higTrig_rect.top() + i5)); + + p.drawLine(QPointF(negTrig_rect.left() + i5, negTrig_rect.top() + i5), + QPointF(negTrig_rect.center().x(), negTrig_rect.top() + i5)); + p.drawLine(QPointF(negTrig_rect.center().x(), negTrig_rect.top() + i5), + QPointF(negTrig_rect.center().x(), negTrig_rect.bottom() - i5)); + p.drawLine(QPointF(negTrig_rect.center().x(), negTrig_rect.bottom() - i5), + QPointF(negTrig_rect.right() - i5, negTrig_rect.bottom() - i5)); + + p.drawLine(QPointF(lowTrig_rect.left() + i5, lowTrig_rect.bottom() - i5), + QPointF(lowTrig_rect.right() - i5, lowTrig_rect.bottom() - i5)); + + p.drawLine(QPointF(edgeTrig_rect.left() + i5, edgeTrig_rect.top() + i5), + QPointF(edgeTrig_rect.center().x() - i2, edgeTrig_rect.top() + i5)); + p.drawLine(QPointF(edgeTrig_rect.center().x() + i2 , edgeTrig_rect.top() + i5), + QPointF(edgeTrig_rect.right() - i5, edgeTrig_rect.top() + i5)); + p.drawLine(QPointF(edgeTrig_rect.center().x(), edgeTrig_rect.top() + i7), + QPointF(edgeTrig_rect.center().x(), edgeTrig_rect.bottom() - i7)); + p.drawLine(QPointF(edgeTrig_rect.left() + i5, edgeTrig_rect.bottom() - i5), + QPointF(edgeTrig_rect.center().x() - i2, edgeTrig_rect.bottom() - i5)); + p.drawLine(QPointF(edgeTrig_rect.center().x() + i2, edgeTrig_rect.bottom() - i5), + QPointF(edgeTrig_rect.right() - i5, edgeTrig_rect.bottom() - i5)); } bool LogicSignal::measure(const QPointF &p, uint64_t &index0, uint64_t &index1, uint64_t &index2) @@ -304,7 +337,7 @@ bool LogicSignal::measure(const QPointF &p, uint64_t &index0, uint64_t &index1, const uint64_t end = _data->get_ring_sample_count() - 1; uint64_t index = _data->samplerate() * _view->scale() * (_view->x_offset() + p.x()); - + if (index > end){ return false; } @@ -459,7 +492,7 @@ bool LogicSignal::edges(const QPointF &p, uint64_t start, uint64_t &rising, uint } bool LogicSignal::edges(uint64_t end, uint64_t start, uint64_t &rising, uint64_t &falling) -{ +{ if (_data->empty() || !_data->has_data(_probe->index)) return false; @@ -517,33 +550,36 @@ bool LogicSignal::mouse_press(int right, const QPoint pt) QRectF LogicSignal::get_rect(LogicSetRegions type, int y, int right) { - const QSizeF name_size(right - get_leftWidth() - get_rightWidth(), SquareWidth); + const int squareWidth = get_squareWidth(); + const int squareMargin = get_squareMargin(); + const QSizeF name_size(right - get_leftWidth() - get_rightWidth(), squareWidth); + const int base_x = get_leftWidth() + name_size.width() + squareMargin; if (type == POSTRIG) return QRectF( - get_leftWidth() + name_size.width() + Margin, - y - SquareWidth / 2, - SquareWidth, SquareWidth); + base_x, + y - squareWidth / 2, + squareWidth, squareWidth); else if (type == HIGTRIG) return QRectF( - get_leftWidth() + name_size.width() + SquareWidth + Margin, - y - SquareWidth / 2, - SquareWidth, SquareWidth); + base_x + squareWidth, + y - squareWidth / 2, + squareWidth, squareWidth); else if (type == NEGTRIG) return QRectF( - get_leftWidth() + name_size.width() + 2 * SquareWidth + Margin, - y - SquareWidth / 2, - SquareWidth, SquareWidth); + base_x + 2 * squareWidth, + y - squareWidth / 2, + squareWidth, squareWidth); else if (type == LOWTRIG) return QRectF( - get_leftWidth() + name_size.width() + 3 * SquareWidth + Margin, - y - SquareWidth / 2, - SquareWidth, SquareWidth); + base_x + 3 * squareWidth, + y - squareWidth / 2, + squareWidth, squareWidth); else if (type == EDGTRIG) return QRectF( - get_leftWidth() + name_size.width() + 4 * SquareWidth + Margin, - y - SquareWidth / 2, - SquareWidth, SquareWidth); + base_x + 4 * squareWidth, + y - squareWidth / 2, + squareWidth, squareWidth); else return QRectF(0, 0, 0, 0); } diff --git a/DSView/pv/view/logicsignal.h b/DSView/pv/view/logicsignal.h index 2ecab7c9d..31d7f3e4a 100644 --- a/DSView/pv/view/logicsignal.h +++ b/DSView/pv/view/logicsignal.h @@ -85,6 +85,10 @@ class LogicSignal : public Signal bool commit_trig(); + // Draws the dotted zero line plus a row separator so adjacent channels + // are visually distinguishable. + void paint_back(QPainter &p, int left, int right, QColor fore, QColor back); + /** * Paints the signal with a QPainter * @param p the QPainter to paint into. diff --git a/DSView/pv/view/ruler.cpp b/DSView/pv/view/ruler.cpp index 74c750a19..dacbfbe47 100644 --- a/DSView/pv/view/ruler.cpp +++ b/DSView/pv/view/ruler.cpp @@ -246,9 +246,7 @@ void Ruler::paintEvent(QPaintEvent*) style()->drawPrimitive(QStyle::PE_Widget, &o, &p, this); QFont font = p.font(); - float fSize = AppConfig::Instance().appOptions.fontSize; - if (fSize > 10) - fSize = 10; + float fSize = AppConfig::Instance().GetTraceFontSize(); font.setPointSizeF(fSize); p.setFont(font); @@ -759,7 +757,6 @@ void Ruler::draw_cursor_sel(QPainter &p) if (!cursor_list.empty()) { int index = 1; - auto i = cursor_list.begin(); for (auto curosr : cursor_list) { const QRectF cursorRect = get_cursor_sel_rect(index); diff --git a/DSView/pv/view/trace.cpp b/DSView/pv/view/trace.cpp index fcd58f26a..a8d5bed43 100644 --- a/DSView/pv/view/trace.cpp +++ b/DSView/pv/view/trace.cpp @@ -98,13 +98,58 @@ Trace::Trace(const Trace &t) : int Trace::get_name_width() { QFont font; - float fSize = AppConfig::Instance().appOptions.fontSize; - font.setPointSizeF(fSize <= 10 ? fSize : 10); + float fSize = AppConfig::Instance().GetTraceFontSize(); + // Keep the reserved label width in sync with the scaled label font so + // channel names are not clipped when the trace height is scaled up. + fSize *= get_label_scale(); + font.setPointSizeF(fSize); QFontMetrics fm(font); return fm.boundingRect(get_name()).width(); } +double Trace::get_label_scale() +{ + return (_view != NULL) ? _view->get_trace_font_scale() : 1.0; +} + +// Ratio between the currently configured trace font size and the font size +// the Margin/SquareWidth pixel constants were tuned against. Without this, +// the per-channel config boxes (AC/DC, AUTO, x1/x10/x100, etc.) stayed a +// fixed pixel size regardless of the font size setting, while the text +// drawn inside them (sized off the same GetTraceFontSize() value, see +// Viewport::paintEvent) grew - so bigger font settings just clipped the +// box text instead of growing the box with it. +static double square_font_ratio() +{ + return AppConfig::Instance().GetTraceFontSize() / Trace::BaseFontSize; +} + +int Trace::get_squareWidth() +{ + return (int)(SquareWidth * square_font_ratio() * get_label_scale()); +} + +int Trace::get_squareMargin() +{ + return (int)(Margin * square_font_ratio() * get_label_scale()); +} + +int Trace::get_leftWidth() +{ + return get_squareWidth() / 2 + get_squareMargin(); +} + +int Trace::get_rightWidth() +{ + return 2 * get_squareMargin() + _typeWidth * get_squareWidth() + 1.5 * get_squareWidth(); +} + +int Trace::get_headerHeight() +{ + return get_squareWidth(); +} + void Trace::set_name(QString name) { _name = name; @@ -156,6 +201,15 @@ void Trace::paint_prepare() _view->set_trig_hoff(0); } +QColor Trace::get_default_colour() +{ + if (_type == SR_CHANNEL_DSO || _type == SR_CHANNEL_FFT || + _type == SR_CHANNEL_ANALOG || _type == SR_CHANNEL_MATH || _index_list.empty()) + return QColor(); + + return PROBE_COLORS[*_index_list.begin() % countof(PROBE_COLORS)]; +} + void Trace::paint_back(QPainter &p, int left, int right, QColor fore, QColor back) { (void)back; @@ -201,8 +255,9 @@ void Trace::paint_label(QPainter &p, int right, const QPoint pt, QColor fore) // Paint the ColorButton QColor foreBack = fore; foreBack.setAlpha(View::BackAlpha); + QColor defaultColour = get_default_colour(); p.setPen(Qt::transparent); - p.setBrush(enabled() ? (_colour.isValid() ? _colour : fore) : foreBack); + p.setBrush(enabled() ? (_colour.isValid() ? _colour : (defaultColour.isValid() ? defaultColour : fore)) : foreBack); p.drawRect(color_rect); if (_type == SR_CHANNEL_DSO || @@ -374,10 +429,10 @@ int Trace::rows_size() QRectF Trace::get_rect(const char *s, int y, int right) { - const QSizeF color_size(get_leftWidth() - Margin, SquareWidth); - // const QSizeF name_size(right - get_leftWidth() - get_rightWidth(), SquareWidth); - const QSizeF name_size(right - get_leftWidth() - get_rightWidth(), SquareWidth); - const QSizeF label_size(SquareWidth, SquareWidth); + const int squareWidth = get_squareWidth(); + const QSizeF color_size(get_leftWidth() - get_squareMargin(), squareWidth); + const QSizeF name_size(right - get_leftWidth() - get_rightWidth(), squareWidth); + const QSizeF label_size(squareWidth, squareWidth); if (!strcmp(s, "name")) return QRectF( @@ -397,8 +452,8 @@ QRectF Trace::get_rect(const char *s, int y, int right) else return QRectF( 2, - y - SquareWidth / 2, - SquareWidth, SquareWidth); + y - squareWidth / 2, + squareWidth, squareWidth); } } // namespace view diff --git a/DSView/pv/view/trace.h b/DSView/pv/view/trace.h index c33915686..b820eb54b 100644 --- a/DSView/pv/view/trace.h +++ b/DSView/pv/view/trace.h @@ -51,6 +51,10 @@ class Trace : public SelectableItem static const int LabelHitPadding; public: + // The font size (in points) the Margin/SquareWidth pixel constants + // were tuned against - used to scale the boxes with AppConfig's font + // size setting, not just the (independent) trace height factor. + static constexpr double BaseFontSize = 9.0; static const int SquareWidth = 20; static const int COLOR = 1; static const int NAME = 2; @@ -96,6 +100,12 @@ class Trace : public SelectableItem _colour = colour; } + /** + * The colour to fall back to when the signal has no explicit user-set + * colour, so a channel's waveform matches the colour of its label flag. + */ + QColor get_default_colour(); + /** * Gets the vertical layout offset of this signal. */ @@ -161,17 +171,23 @@ class Trace : public SelectableItem /** * Geom */ - inline int get_leftWidth(){ - return SquareWidth/2 + Margin; - } - inline int get_rightWidth(){ - return 2 * Margin + _typeWidth * SquareWidth + 1.5 * SquareWidth; - } + /** + * Scale applied to the header-label geometry (square/box sizes, margins) + * so the labels and trigger buttons grow together with the trace height. + * Returns 1.0 when no view is attached or the trace height is not scaled. + */ + double get_label_scale(); - inline int get_headerHeight(){ - return SquareWidth; - } + // Square and margin sizes scaled by get_label_scale(). + int get_squareWidth(); + int get_squareMargin(); + + int get_leftWidth(); + + int get_rightWidth(); + + int get_headerHeight(); /** * Gets the old vertical layout offset of this signal. diff --git a/DSView/pv/view/view.cpp b/DSView/pv/view/view.cpp index 8c7ada50a..2921d1407 100644 --- a/DSView/pv/view/view.cpp +++ b/DSView/pv/view/view.cpp @@ -4,6 +4,7 @@ * * Copyright (C) 2012 Joel Holdsworth * Copyright (C) 2013 DreamSourceLab + * Copyright (C) 2026 Schildkroet * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,6 +28,7 @@ #include #include #include +#include #include #include "groupsignal.h" @@ -63,9 +65,16 @@ const int View::RulerHeight = 50; const int View::MaxScrollValue = INT_MAX / 2; const int View::HeightUnit = 20; // also serves as minimum signal height -const int View::SignalMargin = 3; +const int View::SignalMargin = 12; +const int View::SignalMarginCompact = 3; const int View::SignalSnapGridSize = 10; +int View::get_signal_margin() +{ + return AppConfig::Instance().appOptions.logicChannelDivider + ? SignalMargin : SignalMarginCompact; +} + const QColor View::CursorAreaColour(220, 231, 243); const QSizeF View::LabelPadding(4, 4); const QString View::Unknown_Str = "########"; @@ -99,7 +108,7 @@ View::View(SigSession *session, pv::toolbars::SamplingBar *sampling_bar, QWidget _dso_auto(true), _show_lissajous(false), _back_ready(false) -{ +{ _trig_cursor = NULL; _search_cursor = NULL; _cali = NULL; @@ -107,11 +116,17 @@ View::View(SigSession *session, pv::toolbars::SamplingBar *sampling_bar, QWidget _session = session; _device_agent = session->get_device(); + _trace_height_factor = AppConfig::Instance().appOptions.traceHeightFactor; + if (_trace_height_factor < MinTraceHeightFactor || _trace_height_factor > MaxTraceHeightFactor) + _trace_height_factor = 1.0; + + _dso_split_channels = AppConfig::Instance().appOptions.dsoSplitChannels; + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); // setWidgetResizable(true); // setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - + // trace viewport map _trace_view_map[SR_CHANNEL_LOGIC] = TIME_VIEW; _trace_view_map[SR_CHANNEL_GROUP] = TIME_VIEW; @@ -126,19 +141,19 @@ View::View(SigSession *session, pv::toolbars::SamplingBar *sampling_bar, QWidget _ruler = new Ruler(*this); _header = new Header(*this); _devmode = new DevMode(this, session); - - setViewportMargins(headerWidth(), RulerHeight, 0, 0); + + setViewportMargins(headerWidth(), RulerHeight, 0, get_bottom_margin()); // windows splitter _time_viewport = new Viewport(*this, TIME_VIEW); _time_viewport->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); _time_viewport->setMinimumHeight(100); - + _fft_viewport = new Viewport(*this, FFT_VIEW); _fft_viewport->setVisible(false); _fft_viewport->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); _fft_viewport->setMinimumHeight(100); - + _vsplitter = new QSplitter(this); _vsplitter->setOrientation(Qt::Vertical); _vsplitter->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); @@ -159,13 +174,13 @@ View::View(SigSession *session, pv::toolbars::SamplingBar *sampling_bar, QWidget layout->setContentsMargins(0,0,0,0); _viewcenter->setLayout(layout); layout->addWidget(_vsplitter, 0, 0); - QVBoxLayout* statusLayout = new QVBoxLayout(this); - statusLayout->setSpacing(0); - statusLayout->setContentsMargins(0,0,verticalScrollBar()->geometry().width()+2, horizontalScrollBar()->geometry().height()+1); + _statusLayout = new QVBoxLayout(this); + _statusLayout->setSpacing(0); + _statusLayout->setContentsMargins(0,0,verticalScrollBar()->geometry().width()+2, horizontalScrollBar()->geometry().height()+1); _viewbottom = new ViewStatus(_session, *this); _viewbottom->setFixedHeight(StatusHeight); - setLayout(statusLayout); - statusLayout->addWidget(_viewbottom, 0, Qt::AlignBottom); + setLayout(_statusLayout); + _statusLayout->addWidget(_viewbottom, 0, Qt::AlignBottom); #ifdef Q_OS_DARWIN QWidget *lineSpan = new QWidget(this); @@ -205,7 +220,7 @@ View::View(SigSession *session, pv::toolbars::SamplingBar *sampling_bar, QWidget connect(_fft_viewport, SIGNAL(measure_updated()), this, SLOT(on_measure_updated())); connect(_vsplitter, SIGNAL(splitterMoved(int,int)), this, SLOT(splitterMoved(int, int))); - + connect(_header, SIGNAL(traces_moved()),this, SLOT(on_traces_moved())); connect(_header, SIGNAL(header_updated()),this, SLOT(header_updated())); @@ -242,13 +257,13 @@ void View::capture_init() show_trig_cursor(true); else if (!_session->is_repeating()) show_trig_cursor(false); - + _maxscale = _session->cur_sampletime() / (width * MaxViewRate); if (mode == ANALOG){ set_scale_offset(_maxscale, 0); } - + status_clear(); _trig_hoff = 0; @@ -300,7 +315,7 @@ bool View::zoom(double steps, int offset) if (_device_agent->get_work_mode() != DSO) { _scale *= std::pow(3.0/2.0, -steps); _scale = max(min(_scale, _maxscale), _minscale); - } + } else { if (_session->is_running_status() && _session->is_instant()){ return ret; @@ -315,7 +330,7 @@ bool View::zoom(double steps, int offset) if (hori_res > 0) { const double scale = _session->cur_view_time() / width; _scale = max(min(scale, _maxscale), _minscale); - } + } else { ret = false; } @@ -334,6 +349,47 @@ bool View::zoom(double steps, int offset) return ret; } +void View::vzoom(double steps) +{ + if (_device_agent->have_instance() == false) + return; + + // Vertical scaling only makes sense for the logic trace window. + if (_device_agent->get_work_mode() != LOGIC) + return; + + // Halve the step size (relative to horizontal zoom) for finer adjustment. + double factor = _trace_height_factor * std::pow(3.0/2.0, steps * 0.5); + set_trace_height_factor(factor); +} + +void View::set_trace_height_factor(double factor) +{ + factor = max(min(factor, MaxTraceHeightFactor), MinTraceHeightFactor); + + if (factor == _trace_height_factor) + return; + + _trace_height_factor = factor; + + AppConfig &app = AppConfig::Instance(); + app.appOptions.traceHeightFactor = factor; + app.SaveApp(); + + signals_changed(NULL); + _header->update(); + viewport_update(); + update_scroll(); +} + +double View::get_trace_font_scale() +{ + // Vertical scaling only applies to the logic trace window (see vzoom()). + if (_device_agent->have_instance() && _device_agent->get_work_mode() == LOGIC) + return _trace_height_factor; + return 1.0; +} + void View::timebase_changed() { int width = get_view_width(); @@ -372,7 +428,7 @@ void View::set_scale_offset(double scale, int64_t offset) } void View::set_preScale_preOffset() -{ +{ set_scale_offset(_preScale, _preOffset); } @@ -381,16 +437,16 @@ void View::get_traces(int type, std::vector &traces) assert(_session); auto &sigs = _session->get_signals(); - + const auto &decode_sigs = _session->get_decode_signals(); - + const auto &spectrums = _session->get_spectrum_traces(); - + for(auto t : sigs) { if (type == ALL_VIEW || _trace_view_map[t->get_type()] == type) traces.push_back(t); } - + for(auto t : decode_sigs) { if (type == ALL_VIEW || _trace_view_map[t->get_type()] == type) traces.push_back(t); @@ -429,11 +485,11 @@ bool View::compare_trace_v_offsets(const Trace *a, const Trace *b) if (a1->get_type() != b1->get_type()){ v1 = a1->get_type(); v2 = b1->get_type(); - } + } else if (a1->get_type() == SR_CHANNEL_DSO || a1->get_type() == SR_CHANNEL_ANALOG){ v1 = a1->get_index(); v2 = b1->get_index(); - } + } else{ v1 = a1->get_v_offset(); v2 = b1->get_v_offset(); @@ -509,7 +565,7 @@ void View::receive_end() bool ret; ret = _device_agent->get_config_bool(SR_CONF_RLE, rle); - + if (ret && rle) { ret = _device_agent->get_config_uint64(SR_CONF_ACTUAL_SAMPLES, actual_samples); if (ret) { @@ -517,7 +573,7 @@ void View::receive_end() _viewbottom->set_rle_depth(actual_samples); } } - } + } } _time_viewport->unshow_wait_trigger(); } @@ -531,7 +587,7 @@ void View::receive_trigger(quint64 trig_pos1) } void View::set_trig_cursor_posistion(uint64_t trig_pos) -{ +{ const double time = trig_pos * 1.0 / _session->cur_snap_samplerate(); _trig_cursor->set_index(trig_pos); @@ -564,7 +620,7 @@ void View::set_trig_pos(int percent) } void View::set_search_pos(uint64_t search_pos, bool hit) -{ +{ QColor fore(QWidget::palette().color(QWidget::foregroundRole())); fore.setAlpha(View::BackAlpha); @@ -585,7 +641,7 @@ void View::set_search_pos(uint64_t search_pos, bool hit) } void View::normalize_layout() -{ +{ int v_min = INT_MAX; std::vector traces; get_traces(ALL_VIEW, traces); @@ -601,7 +657,7 @@ void View::normalize_layout() } } - const int delta = -min(v_min - (top->get_totalHeight() / 2 + 2 * SignalMargin), 0); + const int delta = -min(v_min - (top->get_totalHeight() / 2 + 2 * get_signal_margin()), 0); verticalScrollBar()->setSliderPosition(delta); v_scroll_value_changed(verticalScrollBar()->sliderPosition()); @@ -643,24 +699,33 @@ void View::update_scroll() _x_offset * 1.0 / length * MaxScrollValue); } - // Set up vertical scrollbar + // Set up vertical scrollbar. Only the time-view traces occupy the + // scrollable top pane; the FFT traces live in the fixed, splitter-sized + // _fft_viewport. Counting the FFT pane here would invent a scroll range + // that has no matching time content, so scrolling it would slide the + // whole left panel out of alignment with the FFT view. std::vector traces; - get_traces(ALL_VIEW, traces); + get_traces(TIME_VIEW, traces); - // Calculate total required height for all traces + // Calculate total required height for the time-pane traces int total_height = 0; for (auto t : traces) { if (t->enabled()) - total_height += t->get_totalHeight() + 2 * SignalMargin; + total_height += t->get_totalHeight() + 2 * get_signal_margin(); } // Make sure we can scroll the last signal past the status bar total_height += StatusHeight; + // Scroll the time pane against its own visible height (which excludes the + // FFT pane when the splitter is showing one). + const int avail_height = _fft_viewport->isVisible() + ? _time_viewport->height() : areaSize.height(); + // Enable vertical scrolling if total height exceeds viewport - if (total_height > areaSize.height()) { - verticalScrollBar()->setRange(0, total_height - areaSize.height()); - verticalScrollBar()->setPageStep(areaSize.height()); + if (total_height > avail_height) { + verticalScrollBar()->setRange(0, total_height - avail_height); + verticalScrollBar()->setPageStep(avail_height); } else { verticalScrollBar()->setRange(0, 0); } @@ -668,18 +733,18 @@ void View::update_scroll() } void View::update_scale_offset() -{ +{ int width = get_view_width(); if (width == 0){ return; } if (_device_agent->get_work_mode() != DSO) { - _maxscale = _session->cur_sampletime() / (width * MaxViewRate); + _maxscale = _session->cur_sampletime() / (width * MaxViewRate); _minscale = (1.0 / _session->cur_snap_samplerate()) / MaxPixelsPerSample; } else { - _scale = _session->cur_view_time() / width; + _scale = _session->cur_view_time() / width; _maxscale = 1e9; _minscale = 1e-15; } @@ -703,7 +768,7 @@ void View::mode_changed() void View::signals_changed(const Trace* eventTrace) { - double actualMargin = SignalMargin; + double actualMargin = get_signal_margin(); int total_rows = 0; int label_size = 0; uint8_t max_height = HeightUnit; @@ -744,7 +809,11 @@ void View::signals_changed(const Trace* eventTrace) t->set_view(this); t->set_viewport(_fft_viewport); t->set_totalHeight(_fft_viewport->height()); - t->set_v_offset(_fft_viewport->geometry().bottom()); + // The header spans the whole view (both splitter panes); the FFT + // viewport's geometry is expressed in that same coordinate space, + // so anchoring the label to the pane's vertical centre keeps the + // left-panel label lined up with the FFT view it belongs to. + t->set_v_offset(_fft_viewport->geometry().center().y()); } } else { @@ -766,40 +835,67 @@ void View::signals_changed(const Trace* eventTrace) if (!time_traces.empty() && _time_viewport) { for(auto t : time_traces) { - if (dynamic_cast(t) || t->enabled()) + // A disabled DSO channel still needs a slot when every channel + // shares one overlaid area (so it keeps a valid, if unused, + // band), but in split mode it shouldn't reserve a whole row of + // waveport space that nothing is drawn into. + bool isDso = (t->signal_type() == SR_CHANNEL_DSO); + bool occupiesRow = t->enabled() || (isDso && !_dso_split_channels); + if (occupiesRow) total_rows += t->rows_size(); if (t->rows_size() != 0) label_size++; } + // Every DSO channel can end up disabled at once in split mode; avoid + // a division by zero below. + total_rows = max(total_rows, 1); + const double height = (_time_viewport->height() - 2 * actualMargin * label_size) * 1.0 / total_rows; if (_device_agent->have_instance() == false){ assert(false); } - + int mode = _device_agent->get_work_mode(); if (mode == LOGIC) { int v; bool ret; + // Minimum row height must accommodate the configured font so that + // in-trace text (e.g. decoder annotations) is not cut off. + QFont trace_font; + trace_font.setPointSizeF(AppConfig::Instance().GetTraceFontSize()); + const int min_row_height = max((int)HeightUnit, + QFontMetrics(trace_font).height() + 4); + ret = _device_agent->get_config_byte(SR_CONF_MAX_HEIGHT_VALUE, v); if (ret) { max_height = (v + 1) * HeightUnit; } if (height < 2*actualMargin) { //actualMargin /= 2; - _signalHeight = max((double)HeightUnit, (_time_viewport->height() + _signalHeight = max((double)min_row_height, (_time_viewport->height() - 2 * actualMargin * label_size) * 1.0 / total_rows); } else { - _signalHeight = max((double)HeightUnit, (height >= max_height) ? max_height : height); + _signalHeight = max((double)min_row_height, (height >= max_height) ? max_height : height); } + + // Apply the user-controlled vertical scaling so logic signals can + // grow to use the full window height (see View::vzoom). + _signalHeight = max((double)min_row_height, _signalHeight * _trace_height_factor); } else if (_device_agent->get_work_mode() == DSO) { - _signalHeight = max((double)HeightUnit, (_header->height() + // Size the channels to the pane they actually live in. Using the + // full-height header would keep them sized for the whole view even + // after the FFT splitter pane has shrunk the time viewport, so the + // channels would overflow the time pane and invent vertical scroll + // range with nothing to scroll to. When no FFT pane is shown the + // time viewport fills the view, so this matches the old behaviour. + _signalHeight = max((double)HeightUnit, (_time_viewport->height() - horizontalScrollBar()->height() - 2 * actualMargin * label_size) * 1.0 / total_rows); } @@ -809,10 +905,10 @@ void View::signals_changed(const Trace* eventTrace) _spanY = _signalHeight + 2 * actualMargin; int next_v_offset = actualMargin; - + //Make list by view-index; if (mode == LOGIC) - { + { time_traces.clear(); std::vector all_traces; @@ -828,7 +924,7 @@ void View::signals_changed(const Trace* eventTrace) time_traces.push_back(t); } - sort(all_traces.begin(), all_traces.end(), compare_trace_view_index); + sort(all_traces.begin(), all_traces.end(), compare_trace_view_index); for(auto t : all_traces){ time_traces.push_back(t); @@ -842,6 +938,16 @@ void View::signals_changed(const Trace* eventTrace) if (t->rows_size() == 0) continue; + // Mirror the total_rows accounting above: a disabled DSO channel + // in split mode gets no row of its own, so it doesn't leave a + // block of empty space where nothing is drawn. + bool isDso = (t->signal_type() == SR_CHANNEL_DSO); + if (!t->enabled() && isDso && _dso_split_channels){ + t->set_totalHeight(0); + t->set_v_offset(next_v_offset); + continue; + } + const double traceHeight = _signalHeight*t->rows_size(); t->set_totalHeight((int)traceHeight); t->set_v_offset(next_v_offset + 0.5 * traceHeight + actualMargin); @@ -850,7 +956,7 @@ void View::signals_changed(const Trace* eventTrace) if (t->signal_type() == SR_CHANNEL_DSO) { auto sig = dynamic_cast(t); - sig->set_scale(sig->get_view_rect().height()); + sig->set_scale(sig->get_view_rect().height()); } else if (t->signal_type() == SR_CHANNEL_ANALOG) { @@ -884,7 +990,7 @@ bool View::eventFilter(QObject *object, QEvent *event) else _hover_point = mouse_event->pos(); } else if (object == _header) - _hover_point = QPoint(0, mouse_event->y()); + _hover_point = QPoint(0, mouse_event->pos().y()); else _hover_point = QPoint(-1, -1); @@ -914,6 +1020,17 @@ bool View::viewportEvent(QEvent *e) } } +int View::get_bottom_margin() +{ + // The status/measurement bar floats over the bottom of the viewport. The + // scrollable time pane can scroll its content clear of it, but the fixed + // FFT splitter pane cannot, so reserve room for the bar only while the FFT + // pane is visible - otherwise it would be cropped underneath the bar. + if (_viewbottom && _fft_viewport && _fft_viewport->isVisible()) + return _viewbottom->height(); + return 0; +} + int View::headerWidth() { int headerWidth = _header->get_nameEditWidth(); @@ -921,7 +1038,7 @@ int View::headerWidth() std::vector traces; get_traces(ALL_VIEW, traces); - if (!traces.empty()) + if (!traces.empty()) { for(auto t : traces){ int w = t->get_name_width() + t->get_leftWidth() + t->get_rightWidth(); @@ -929,7 +1046,7 @@ int View::headerWidth() } } - setViewportMargins(headerWidth, RulerHeight, 0, 0); + setViewportMargins(headerWidth, RulerHeight, 0, get_bottom_margin()); return headerWidth; } @@ -943,7 +1060,7 @@ void View::resizeEvent(QResizeEvent*) } reconstruct(); - setViewportMargins(headerWidth(), RulerHeight, 0, 0); + setViewportMargins(headerWidth(), RulerHeight, 0, get_bottom_margin()); update_margins(); update_scroll(); signals_changed(NULL); @@ -976,7 +1093,7 @@ void View::h_scroll_value_changed(int value) const int range = horizontalScrollBar()->maximum(); if (range < MaxScrollValue) _x_offset = value; - else + else { int64_t length = 0; int64_t offset = 0; @@ -997,9 +1114,12 @@ void View::v_scroll_value_changed(int value) // Track vertical offset _y_offset = value; - // Update vertical positions of all traces based on scroll value + // Only the time-view traces live in the scrollable top pane. The FFT + // traces sit in the separate, splitter-controlled _fft_viewport whose + // content is painted at a fixed viewport-relative position, so scrolling + // them here would drift their header label away from the FFT view. std::vector traces; - get_traces(ALL_VIEW, traces); + get_traces(TIME_VIEW, traces); for (auto t : traces) { if (t->enabled()) { @@ -1013,7 +1133,7 @@ void View::v_scroll_value_changed(int value) void View::data_updated() { - setViewportMargins(headerWidth(), RulerHeight, 0, 0); + setViewportMargins(headerWidth(), RulerHeight, 0, get_bottom_margin()); update_margins(); // Update the scroll bars @@ -1039,7 +1159,22 @@ void View::update_margins() _ruler->setGeometry(_viewcenter->x(), 0, width, _viewcenter->y()); _header->setGeometry(0, _viewcenter->y(), _viewcenter->x(), _viewcenter->height()); _devmode->setGeometry(0, 0, _viewcenter->x(), _viewcenter->y()); - } + } +} + +void View::update_status_margins() +{ + // The bottom/right margins were only ever computed once, at + // construction time, before the scrollbars had a real on-screen + // geometry - and never refreshed afterwards. That was mostly hidden in + // LOGIC mode (a fixed, short StatusHeight), but became visibly wrong in + // DSO mode once _viewbottom grows to DsoStatusHeight for its two-row + // measurement layout: the reserved strip stayed sized for the stale + // scrollbar height, so the lower measurement row overlapped the real + // horizontal scrollbar. Recompute with the scrollbars' current geometry. + _statusLayout->setContentsMargins(0, 0, + verticalScrollBar()->geometry().width() + 2, + horizontalScrollBar()->geometry().height() + 1); } void View::header_updated() @@ -1070,7 +1205,7 @@ void View::on_traces_moved() void View::make_cursors_order() { int dex = 1; - + for (auto cursor : get_cursorList()) { cursor->set_order(dex++); @@ -1085,6 +1220,7 @@ void View::make_cursors_order() void View::add_cursor(QColor color, uint64_t sampleIndex) { + (void)color; Cursor *newCursor = new Cursor(*this, -1, sampleIndex); get_cursorList().push_back(newCursor); make_cursors_order(); @@ -1236,7 +1372,12 @@ void View::on_state_changed(bool stop) QRect View::get_view_rect() { - if (_device_agent->get_work_mode() == DSO) { + // In split mode each channel only owns its own row, so returning the + // first channel's rect here (as the overlaid case does, since every + // channel's rect is the whole viewport there) would confine cursors, + // hit-testing, and status text to that single row instead of the whole + // viewport. + if (_device_agent->get_work_mode() == DSO && !_dso_split_channels) { const auto &sigs = _session->get_signals(); if(sigs.size() > 0) { return sigs[0]->get_view_rect(); @@ -1268,7 +1409,14 @@ int View::get_view_width() int View::get_view_height() { int view_height = 0; - if (_device_agent->get_work_mode() == DSO) { + + // In split mode each DSO channel's get_view_rect() only spans its own + // row, so taking the max over channels would drastically undercount the + // actual visible area (used below to size the vertical scrollbar), + // making it look like there's a lot more content to scroll to than + // there really is. Overlaid channels all still share the full viewport + // height, so this only needs a special case for split mode. + if (_device_agent->get_work_mode() == DSO && !_dso_split_channels) { for(auto s : _session->get_signals()) { view_height = max(view_height, s->get_view_rect().height()); } @@ -1367,6 +1515,20 @@ void View::show_lissajous(bool show) signals_changed(NULL); } +void View::set_dso_split_channels(bool split) +{ + _dso_split_channels = split; + + AppConfig &app = AppConfig::Instance(); + if (app.appOptions.dsoSplitChannels != split){ + app.appOptions.dsoSplitChannels = split; + app.SaveApp(); + } + + signals_changed(NULL); + viewport_update(); +} + void View::show_region(uint64_t start, uint64_t end, bool keep) { assert(start <= end); @@ -1436,6 +1598,7 @@ void View::reconstruct() _viewbottom->setFixedHeight(DsoStatusHeight); else _viewbottom->setFixedHeight(StatusHeight); + update_status_margins(); _viewbottom->reload(); } @@ -1459,13 +1622,17 @@ double View::index2pixel(uint64_t index, bool has_hoff) { const uint64_t rateValue = session().cur_snap_samplerate(); const double scaleValue = scale(); - const int64_t offsetValue = x_offset(); + const int64_t offsetValue = x_offset(); const double hoffValue = trig_hoff(); double pixels = 0; const double samples_per_pixel = rateValue * scaleValue; + if (samples_per_pixel == 0){ + return 0; + } + if (has_hoff){ pixels = index / samples_per_pixel - offsetValue + hoffValue / samples_per_pixel; } @@ -1486,12 +1653,12 @@ double View::index2pixel(uint64_t index, bool has_hoff) } uint64_t View::pixel2index(double pixel) -{ +{ const uint64_t rateValue = session().cur_snap_samplerate(); const double scaleValue = scale(); - const int64_t offsetValue = x_offset(); + const int64_t offsetValue = x_offset(); const double hoffValue = trig_hoff(); - + const double samples_per_pixel = rateValue * scaleValue; const double index = (pixel + offsetValue) * samples_per_pixel - hoffValue; @@ -1507,7 +1674,7 @@ void View::set_receive_len(uint64_t len) { if (_time_viewport) _time_viewport->set_receive_len(len); - + if (_fft_viewport && _session->get_device()->get_work_mode() == DSO) _fft_viewport->set_receive_len(len); } @@ -1531,10 +1698,10 @@ void View::check_calibration() if (_device_agent->get_work_mode() == DSO){ bool cali = false; _device_agent->get_config_bool(SR_CONF_CALI, cali); - + if (cali) { show_calibration(); - } + } } } @@ -1564,7 +1731,7 @@ void View::auto_set_max_scale() { _maxscale = limitTime / (width * MaxViewRate); set_scale(_maxscale); - } + } } int View::get_body_width() @@ -1599,7 +1766,7 @@ void View::check_measure() } std::list& View::get_cursorList() -{ +{ if (_session->get_device()->get_work_mode() == LOGIC){ return _logic_cursors; } @@ -1629,16 +1796,16 @@ Cursor* View::get_cursor_by_index(int index) void View::UpdateLanguage() { - + } void View::UpdateTheme() { - + } void View::UpdateFont() -{ +{ update_font(); } diff --git a/DSView/pv/view/view.h b/DSView/pv/view/view.h index b342d0cff..dce9f78f4 100644 --- a/DSView/pv/view/view.h +++ b/DSView/pv/view/view.h @@ -4,6 +4,7 @@ * * Copyright (C) 2012 Joel Holdsworth * Copyright (C) 2013 DreamSourceLab + * Copyright (C) 2026 Schildkroet * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -32,6 +33,7 @@ #include #include #include +#include #include "../toolbars/samplingbar.h" @@ -83,15 +85,24 @@ class View : public QScrollArea, public IUiWindow public: //static const int SignalHeight; - static const int SignalMargin; + static const int SignalMargin; // inter-channel spacing with divider + static const int SignalMarginCompact; // original spacing without divider static const int SignalSnapGridSize; + // Effective inter-channel margin: the compact spacing is restored when the + // logic channel divider line is disabled (see AppConfig::logicChannelDivider). + static int get_signal_margin(); + static const QColor CursorAreaColour; static const QSizeF LabelPadding; static const QString Unknown_Str; static const int WellSamplesPerPixel = 2048; static constexpr double MaxViewRate = 1.0; + + // Bounds for the vertical (trace height) scaling factor. + static constexpr double MinTraceHeightFactor = 0.5; + static constexpr double MaxTraceHeightFactor = 20.0; static const int MaxPixelsPerSample = 100; static const int StatusHeight = 20; @@ -168,6 +179,20 @@ class View : public QScrollArea, public IUiWindow void zoom(double steps); bool zoom(double steps, int offset); + /** + * Scales the vertical size of the traces (logic mode) so signals can + * use more of the available window height. Positive steps grow the + * traces, negative steps shrink them. The factor is persisted. + */ + void vzoom(double steps); + + /** + * Sets the vertical trace-height scaling factor directly (logic mode). + * The value is clamped to [MinTraceHeightFactor, MaxTraceHeightFactor] + * and persisted. Passing 1.0 resets the y-axis zoom to its default. + */ + void set_trace_height_factor(double factor); + /** * Sets the scale and offset. * @param scale The new view scale in seconds per pixel. @@ -201,8 +226,30 @@ class View : public QScrollArea, public IUiWindow return _signalHeight; } + inline double get_trace_height_factor(){ + return _trace_height_factor; + } + + inline bool get_dso_split_channels(){ + return _dso_split_channels; + } + + void set_dso_split_channels(bool split); + + /** + * Scale applied to trace-area text so it grows together with the trace + * height. Returns the vertical scaling factor in logic mode and 1.0 in + * every other mode (where trace height is not scaled). + */ + double get_trace_font_scale(); + int headerWidth(); + // Bottom viewport margin reserved for the status/measurement bar. Non-zero + // only when the FFT splitter pane is shown, so that fixed pane isn't + // cropped underneath the bar. + int get_bottom_margin(); + inline Ruler* get_ruler(){ return _ruler; } @@ -358,7 +405,14 @@ class View : public QScrollArea, public IUiWindow static bool compare_trace_v_offsets( const Trace *a, const Trace *b); void get_scroll_layout(int64_t &length, int64_t &offset); void update_scroll(); - void update_margins(); + void update_margins(); + // Re-reserves bottom/right space in _statusLayout for the real + // scrollbars, using their current (not construction-time) geometry - + // the ViewStatus (_viewbottom) widget can grow taller (DSO's 2-row + // measurement layout) after construction, so the scrollbar-clearance + // margin has to be refreshed alongside it, or the lower measurement + // row overlaps the horizontal scrollbar. + void update_status_margins(); void set_scale(double scale); void clear(); @@ -443,10 +497,13 @@ private slots: pv::toolbars::SamplingBar *_sampling_bar; QWidget *_viewcenter; - ViewStatus *_viewbottom; + // Zero-initialised so get_bottom_margin(), reached via headerWidth() + // during construction, sees null (not garbage) before these are assigned. + ViewStatus *_viewbottom = nullptr; + QVBoxLayout *_statusLayout; QSplitter *_vsplitter; Viewport *_time_viewport; - Viewport *_fft_viewport; + Viewport *_fft_viewport = nullptr; Viewport *_active_viewport; LissajousFigure *_lissajous; std::list _viewport_list; @@ -468,6 +525,8 @@ private slots: int64_t _preOffset; int _spanY; int _signalHeight; + double _trace_height_factor; + bool _dso_split_channels; bool _updating_scroll; // trigger position fix diff --git a/DSView/pv/view/viewport.cpp b/DSView/pv/view/viewport.cpp index b72fc5177..6eab54276 100644 --- a/DSView/pv/view/viewport.cpp +++ b/DSView/pv/view/viewport.cpp @@ -4,6 +4,7 @@ * * Copyright (C) 2012 Joel Holdsworth * Copyright (C) 2013 DreamSourceLab + * Copyright (C) 2026 Schildkroet * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -35,7 +36,7 @@ #include #include -#include +#include #include #include #include @@ -43,7 +44,7 @@ #include "../config/appconfig.h" #include "../dsvdef.h" #include "../appcontrol.h" -#include "../log.h" +#include "../log.h" #include "../ui/langresource.h" #include "../ui/fn.h" #include "lissajoustrace.h" @@ -76,7 +77,9 @@ Viewport::Viewport(View &parent, View_type type) : _waiting_trig(0), _dso_trig_moved(false), _curs_moved(false), - _xcurs_moved(false) + _xcurs_moved(false), + _yscale_hint_active(false), + _yscale_badge_pressed(false) { setMouseTracking(true); setAutoFillBackground(true); @@ -93,7 +96,7 @@ Viewport::Viewport(View &parent, View_type type) : _edge_hit = false; _transfer_started = false; _timer_cnt = 0; - + _sample_received = 0; _is_checked_trig = false; @@ -103,17 +106,21 @@ Viewport::Viewport(View &parent, View_type type) : // drag inertial _drag_strength = 0; _drag_timer.setSingleShot(true); - + _cmenu = new QMenu(this); QAction *yAction = _cmenu->addAction(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_ADD_Y_CURSOR), "Add Y-cursor")); QAction *xAction = _cmenu->addAction(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_ADD_X_CURSOR), "Add X-cursor")); _yAction = yAction; _xAction = xAction; - + setContextMenuPolicy(Qt::CustomContextMenu); + // Repeating tick that animates the transient y-scale badge fade-out. + _yscale_hint_timer.setSingleShot(false); + connect(&_trigger_timer, SIGNAL(timeout()),this, SLOT(on_trigger_timer())); - connect(&_drag_timer, SIGNAL(timeout()),this, SLOT(on_drag_timer())); + connect(&_drag_timer, SIGNAL(timeout()),this, SLOT(on_drag_timer())); + connect(&_yscale_hint_timer, SIGNAL(timeout()),this, SLOT(on_yscale_hint_timeout())); connect(yAction, SIGNAL(triggered(bool)), this, SLOT(add_cursor_y())); connect(xAction, SIGNAL(triggered(bool)), this, SLOT(add_cursor_x())); connect(this, SIGNAL(customContextMenuRequested(const QPoint&)),this, SLOT(show_contextmenu(const QPoint&))); @@ -135,7 +142,7 @@ int Viewport::get_total_height() for(auto t : traces) { h += (int)(t->get_totalHeight()); } - h += 2 * View::SignalMargin; + h += 2 * View::get_signal_margin(); return h; } @@ -154,15 +161,15 @@ bool Viewport::event(QEvent *event) void Viewport::paintEvent(QPaintEvent *event) { - (void)event; + (void)event; doPaint(); } void Viewport::doPaint() -{ +{ using pv::view::Signal; - + QStyleOption o; o.initFrom(this); QPainter p(this); @@ -173,9 +180,9 @@ void Viewport::doPaint() style()->drawPrimitive(QStyle::PE_Widget, &o, &p, this); QFont font = p.font(); - float fSize = AppConfig::Instance().appOptions.fontSize; - if (fSize > 10) - fSize = 10; + float fSize = AppConfig::Instance().GetTraceFontSize(); + // Grow trace-area text together with the trace height (logic mode). + fSize *= _view.get_trace_font_scale(); font.setPointSizeF(fSize); p.setFont(font); @@ -184,7 +191,7 @@ void Viewport::doPaint() QColor back(QWidget::palette().color(QWidget::backgroundRole())); fore.setAlpha(View::ForeAlpha); _view.set_back(false); - + std::vector traces; _view.get_traces(_type, traces); @@ -192,11 +199,11 @@ void Viewport::doPaint() t->paint_back(p, 0, _view.get_view_width(), fore, back); if (_view.back_ready()) break; - } + } int mode = _view.session().get_device()->get_work_mode(); - if (mode == LOGIC || _view.session().is_instant()) + if (mode == LOGIC || _view.session().is_instant()) { if (_view.session().is_init_status()) { @@ -207,7 +214,7 @@ void Viewport::doPaint() paintSignals(p, fore, back); } else if (_view.session().is_realtime_refresh()) - { + { _view.session().have_new_realtime_refresh(false); // Try to reset refresh timer. if (_view.session().have_view_data() || _view.session().is_instant()) @@ -223,7 +230,7 @@ void Viewport::doPaint() if (!_transfer_started){ bool triggered; int captured_progress; - + if (_view.session().get_capture_status(triggered, captured_progress)){ _view.show_captured_progress(triggered, captured_progress); } @@ -233,7 +240,7 @@ void Viewport::doPaint() _view.repeat_unshow(); paintProgress(p, fore, back); } - } + } } else { paintSignals(p, fore, back); @@ -247,17 +254,19 @@ void Viewport::doPaint() if (_view.get_signalHeight() != _curSignalHeight) _curSignalHeight = _view.get_signalHeight(); + paintYScaleBadge(p, fore, back); + p.end(); } void Viewport::paintCursors(QPainter &p) -{ +{ const QRect xrect = _view.get_view_rect(); auto &cursor_list = _view.get_cursorList(); if (_view.cursors_shown() && _type == TIME_VIEW) { - for (auto cursor : cursor_list) { + for (auto cursor : cursor_list) { const int64_t cursorX = _view.index2pixel(cursor->index()); if (xrect.contains(_view.hover_point().x(), _view.hover_point().y()) && qAbs(cursorX - _view.hover_point().x()) <= HitCursorMargin) @@ -268,12 +277,80 @@ void Viewport::paintCursors(QPainter &p) } } +void Viewport::paintYScaleBadge(QPainter &p, QColor fore, QColor back) +{ + // Only meaningful for the logic trace window, where vzoom() applies. + if (_type != TIME_VIEW || + _view.session().get_device()->get_work_mode() != LOGIC) { + _yscale_badge_rect = QRect(); + return; + } + + // Shown transiently after a vertical zoom / reset, then fades out. + if (!_yscale_hint_active) { + _yscale_badge_rect = QRect(); + return; + } + + // Full opacity, then a linear fade over the last YScaleBadgeFadeMs. + const qint64 elapsed = _yscale_hint_clock.elapsed(); + const qint64 fade_start = YScaleBadgeDurationMs - YScaleBadgeFadeMs; + double opacity = 1.0; + if (elapsed > fade_start) + opacity = max(0.0, 1.0 - double(elapsed - fade_start) / YScaleBadgeFadeMs); + + const double factor = _view.get_trace_height_factor(); + const QString text = QString("Y-scale %1x").arg(factor, 0, 'f', 2); + const QString glyph = QString::fromUtf8(" \xE2\x9F\xB2"); // U+27F2 reset arrow + + QFont font = p.font(); + font.setPointSizeF(AppConfig::Instance().GetTraceFontSize()); + const QFontMetrics fm(font); + + const int padX = 8; + const int padY = 4; + const int textW = fm.horizontalAdvance(text + glyph); + const int textH = fm.height(); + + const QRect view_rect = _view.get_view_rect(); + const int margin = 6; + QRect badge(view_rect.right() - textW - 2 * padX - margin, + view_rect.top() + margin, + textW + 2 * padX, + textH + 2 * padY); + + p.save(); + p.setRenderHint(QPainter::Antialiasing, true); + p.setOpacity(opacity); + p.setFont(font); + + QColor bg = back; + bg.setAlpha(220); + p.setPen(QPen(View::Blue, 1)); + p.setBrush(bg); + p.drawRoundedRect(badge, 4, 4); + + fore.setAlpha(255); + p.setPen(fore); + p.drawText(QRect(badge.left() + padX, badge.top(), fm.horizontalAdvance(text) + 1, badge.height()), + Qt::AlignVCenter | Qt::AlignLeft, text); + p.setPen(View::Blue); + p.drawText(QRect(badge.left() + padX + fm.horizontalAdvance(text), badge.top(), + fm.horizontalAdvance(glyph) + padX, badge.height()), + Qt::AlignVCenter | Qt::AlignLeft, glyph); + + p.restore(); + + // Store hit-area so a click resets the y-scale (#5). + _yscale_badge_rect = badge; +} + void Viewport::paintSignals(QPainter &p, QColor fore, QColor back) -{ +{ std::vector traces; _view.get_traces(_type, traces); - if (_view.session().get_device()->get_work_mode() == LOGIC) + if (_view.session().get_device()->get_work_mode() == LOGIC) { bool bFirst = true; uint64_t end_align_sample; @@ -284,27 +361,29 @@ void Viewport::paintSignals(QPainter &p, QColor fore, QColor back) if (t->signal_type() == SR_CHANNEL_LOGIC) { LogicSignal *logic_signal = (LogicSignal*)t; - + if (bFirst) end_align_sample = logic_signal->data()->get_ring_sample_count(); - + logic_signal->paint_mid_align_sample(p, 0, t->get_view_rect().right(), fore, back, end_align_sample); bFirst = false; } else{ t->paint_mid(p, 0, t->get_view_rect().right(), fore, back); - } - } + } + } } - } + } else { if (_view.scale() != _curScale || _view.x_offset() != _curOffset || _view.get_signalHeight() != _curSignalHeight || + _view.y_offset() != _curYOffset || _need_update) { _curScale = _view.scale(); _curOffset = _view.x_offset(); _curSignalHeight = _view.get_signalHeight(); + _curYOffset = _view.y_offset(); _pixmap = QPixmap(size()); _pixmap.fill(Qt::transparent); @@ -321,24 +400,27 @@ void Viewport::paintSignals(QPainter &p, QColor fore, QColor back) isLissa = true; } } - + for(auto t : traces) { if (t->enabled()) - { + { if (isLissa && t->signal_type() == SR_CHANNEL_DSO) continue; if (isLissa && t->signal_type() == SR_CHANNEL_MATH) continue; - + t->paint_mid(dbp, 0, t->get_view_rect().right(), fore, back); - } + } } _need_update = false; } p.drawPixmap(0, 0, _pixmap); } + // frozen reference waveforms, overlaid on top of the live traces + paint_ref_waves(p); + // plot cursors paintCursors(p); @@ -413,7 +495,7 @@ void Viewport::paintSignals(QPainter &p, QColor fore, QColor back) //plot trigger information if (_view.session().get_device()->get_work_mode() == DSO - && _view.session().is_running_status()) + && _view.session().is_running_status()) { int type; bool roll = false; @@ -428,13 +510,13 @@ void Viewport::paintSignals(QPainter &p, QColor fore, QColor back) if (type == DSO_TRIGGER_AUTO && roll) { type_str = L_S(STR_PAGE_DLG, S_ID(IDS_DLG_AUTO_ROLL), "Auto(Roll)"); - + if (_view.session().is_instant()){ type_str += ", "; type_str += L_S(STR_PAGE_DLG, S_ID(IDS_DLG_VIEW_CAPTURE), "Capturing"); bDot = true; } - } + } else if (type == DSO_TRIGGER_AUTO && !_view.session().trigd()) { type_str = L_S(STR_PAGE_DLG, S_ID(IDS_DLG_AUTO), "Auto"); @@ -443,11 +525,11 @@ void Viewport::paintSignals(QPainter &p, QColor fore, QColor back) type_str += L_S(STR_PAGE_DLG, S_ID(IDS_DLG_VIEW_CAPTURE), "Capturing"); bDot = true; } - } + } else if (_waiting_trig > 0) { - type_str = L_S(STR_PAGE_DLG, S_ID(IDS_DLG_WAITING_TRIG), "Waiting Trig"); + type_str = L_S(STR_PAGE_DLG, S_ID(IDS_DLG_WAITING_TRIG), "Waiting Trig"); bDot = true; - } + } else { type_str = L_S(STR_PAGE_DLG, S_ID(IDS_DLG_TRIG_D), "Trig'd"); } @@ -482,14 +564,105 @@ void Viewport::paintSignals(QPainter &p, QColor fore, QColor back) p.setPen(QColor(255,0,0,200)); p.drawText(_view.get_view_rect(), Qt::AlignRight | Qt::AlignTop, data_status); p.setPen(fore); - } - } + } + } } } } +void Viewport::paint_ref_waves(QPainter &p) +{ + if (_type != TIME_VIEW) + return; + if (_view.session().get_device()->get_work_mode() != DSO) + return; + + auto &refs = _view.session().get_ref_waves(); + if (refs.empty()) + return; + + std::vector traces; + _view.get_traces(TIME_VIEW, traces); + + const double vscale = _view.scale(); + const int64_t x_offset = _view.x_offset(); + const double trig_hoff = _view.trig_hoff(); + const int left = 0; + const int width = _view.get_view_width(); + + if (vscale <= 0) + return; + + for (auto &rw : refs) { + // The reference is rendered with its source channel's current vertical + // scaling, so it tracks the channel's dial and stays on the grid. + DsoSignal *sig = NULL; + for (auto t : traces) { + if (t->signal_type() == SR_CHANNEL_DSO && t->get_index() == rw.index) { + sig = (DsoSignal*)t; + break; + } + } + if (sig == NULL || !sig->enabled()) + continue; + + const int64_t n = (int64_t)rw.samples.size(); + if (n < 2 || rw.samplerate <= 0) + continue; + + const float zeroY = sig->get_zero_vpos(); + const int hw_offset = sig->get_hw_offset(); + const float sscale = sig->get_scale(); + const QRect vrect = sig->get_view_rect(); + const float top = vrect.top(); + const float bottom = vrect.bottom(); + + const double samples_per_pixel = rw.samplerate * vscale; + if (samples_per_pixel <= 0) + continue; + const double pixels_per_sample = 1.0 / samples_per_pixel; + const int64_t last_sample = n - 1; + const double start = x_offset * samples_per_pixel - trig_hoff; + const double end = start + samples_per_pixel * width; + const int64_t start_sample = + min(max((int64_t)floor(start), (int64_t)0), last_sample); + const int64_t end_sample = + min(max((int64_t)ceil(end) + 1, (int64_t)0), last_sample); + if (end_sample <= start_sample) + continue; + + const int64_t count = end_sample - start_sample + 1; + // Reuse the scratch buffer across calls/waves instead of a fresh + // heap array every repaint - resize() only reallocates when growing + // past the buffer's current capacity. + if (_ref_wave_points.size() < count) + _ref_wave_points.resize(count); + QPointF *points = _ref_wave_points.data(); + QPointF *point = points; + float x = (start_sample / samples_per_pixel - x_offset) + left + + trig_hoff * pixels_per_sample; + + for (int64_t s = start_sample; s <= end_sample; s++) { + const uint8_t value = rw.samples[s]; + const float y = min(max(top, zeroY + (value - hw_offset) * sscale), bottom); + *point++ = QPointF(x, y); + x += pixels_per_sample; + } + + QColor c = rw.colour; + c.setAlpha(180); + QPen pen(c); + pen.setStyle(Qt::DashLine); + p.setPen(pen); + p.drawPolyline(points, point - points); + + // Label the reference near its left end. + p.drawText(QPointF(left + 4, top + 12), rw.name); + } +} + void Viewport::get_captured_progress(double &progress, int &progress100) -{ +{ const uint64_t sample_limits = _view.session().cur_samplelimits(); progress = -(_sample_received * 1.0 / sample_limits * 360 * 16); progress100 = ceil(progress / -3.6 / 16); @@ -511,7 +684,7 @@ void Viewport::paintProgress(QPainter &p, QColor fore, QColor back) int captured_progress = 0; get_captured_progress(progress, progress100); - + p.setRenderHint(QPainter::Antialiasing, true); p.setPen(Qt::gray); p.setBrush(Qt::NoBrush); @@ -595,9 +768,9 @@ void Viewport::paintProgress(QPainter &p, QColor fore, QColor back) p.drawEllipse(cenRightPos, trigger_radius, trigger_radius); bool triggered; - + if (_view.session().get_capture_status(triggered, captured_progress)){ - p.setPen(View::Blue); + p.setPen(View::Blue); QFont font = p.font(); float fSize = AppConfig::Instance().appOptions.fontSize; @@ -607,17 +780,17 @@ void Viewport::paintProgress(QPainter &p, QColor fore, QColor back) p.setFont(font); QRect status_rect = QRect(cenPos.x() - radius, cenPos.y() + radius * 0.4, radius * 2, radius * 0.5); - + if (triggered) { p.drawText(status_rect, Qt::AlignCenter | Qt::AlignVCenter, - L_S(STR_PAGE_DLG, S_ID(IDS_DLG_TRIGGERED), "Triggered! ") + QString::number(captured_progress) + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_TRIGGERED), "Triggered! ") + QString::number(captured_progress) + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_CAPTURED), "% Captured")); } else { p.drawText(status_rect, Qt::AlignCenter | Qt::AlignVCenter, - L_S(STR_PAGE_DLG, S_ID(IDS_DLG_WAITING_FOR_TRIGGER), "Waiting for Trigger! ") + QString::number(captured_progress) + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_WAITING_FOR_TRIGGER), "Waiting for Trigger! ") + QString::number(captured_progress) + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_CAPTURED), "% Captured")); } @@ -625,13 +798,13 @@ void Viewport::paintProgress(QPainter &p, QColor fore, QColor back) } } - else { + else { p.setPen(View::Green); QFont font=p.font(); font.setPointSize(50); font.setBold(true); p.setFont(font); - + p.drawText(_view.get_view_rect(), Qt::AlignCenter | Qt::AlignVCenter, QString::number(progress100)+"%"); prgRate(progress100); } @@ -648,7 +821,7 @@ void Viewport::paintProgress(QPainter &p, QColor fore, QColor back) void Viewport::mousePressEvent(QMouseEvent *event) { assert(event); - + _mouse_down_point = event->pos(); _drag_last_mouse_pos = _mouse_down_point; _mouse_down_offset = QPoint(_view.x_offset(), _view.y_offset()); @@ -658,6 +831,17 @@ void Viewport::mousePressEvent(QMouseEvent *event) _drag_delta_t = 0; _drag_delta_x = 0; + // Click on the y-scale badge resets the vertical zoom (logic mode). + // The matching release is swallowed so it doesn't hit the trace below. + if (_action_type == NO_ACTION + && event->button() == Qt::LeftButton + && !_yscale_badge_rect.isEmpty() + && _yscale_badge_rect.contains(event->pos())) { + _yscale_badge_pressed = true; + reset_yscale(); + return; + } + // cancel potential ongoing MOVE action so click/drag is evaluated anew if (_action_type == LOGIC_MOVE) { set_action(NO_ACTION); @@ -673,7 +857,6 @@ void Viewport::mousePressEvent(QMouseEvent *event) else if (_view.session().get_device()->get_work_mode() == DSO) { if (_hover_hit) { const int64_t index = _view.pixel2index(event->pos().x()); - auto &cursor_list = _view.get_cursorList(); _view.add_cursor(index); _view.show_cursors(true); } @@ -684,8 +867,8 @@ void Viewport::mousePressEvent(QMouseEvent *event) event->button() == Qt::LeftButton && _view.session().get_device()->get_work_mode() == DSO) { - for(auto s : _view.session().get_signals()) - { + for(auto s : _view.session().get_signals()) + { if (s->signal_type() == SR_CHANNEL_DSO && s->enabled()) { DsoSignal *dsoSig = (DsoSignal*)s; if (dsoSig->get_trig_rect(0, _view.get_view_width()).contains(_mouse_point)) { @@ -709,9 +892,9 @@ void Viewport::mousePressEvent(QMouseEvent *event) else if (qAbs(searchX - event->pos().x()) <= HitCursorMargin) { _view.get_ruler()->set_grabbed_cursor(_view.get_search_cursor()); set_action(CURS_MOVE); - } + } } - + if (_action_type == NO_ACTION && _view.cursors_shown()) { auto &cursor_list = _view.get_cursorList(); auto i = cursor_list.begin(); @@ -739,7 +922,7 @@ void Viewport::mousePressEvent(QMouseEvent *event) const double cursorX = xrect.left() + (*i)->value(XCursor::XCur_Y)*xrect.width(); const double cursorY0 = xrect.top() + (*i)->value(XCursor::XCur_X0)*xrect.height(); const double cursorY1 = xrect.top() + (*i)->value(XCursor::XCur_X1)*xrect.height(); - + if ((*i)->get_close_rect(xrect).contains(_view.hover_point())) { _view.del_xcursor(*i); if (xcursor_list.empty()) @@ -752,7 +935,7 @@ void Viewport::mousePressEvent(QMouseEvent *event) bool sig_looped = ((*i)->channel() == NULL); bool no_dsoSig = true; - while (true) { + while (true) { if ((*s)->signal_type() == SR_CHANNEL_DSO && (*s)->enabled()) { view::DsoSignal *dsoSig = (view::DsoSignal*)(*s); no_dsoSig = false; @@ -853,13 +1036,13 @@ void Viewport:: mouseMoveEvent(QMouseEvent *event) if ((event->buttons() & Qt::LeftButton) || !(event->buttons() | Qt::NoButton)) { if (_action_type == DSO_TRIG_MOVE) { - if (_drag_sig && _drag_sig->signal_type() == SR_CHANNEL_DSO) { + if (_drag_sig && _drag_sig->signal_type() == SR_CHANNEL_DSO) { view::DsoSignal *dsoSig = (view::DsoSignal*)_drag_sig; dsoSig->set_trig_vpos(event->pos().y()); _dso_trig_moved = true; } } - + if (_action_type == CURS_MOVE) { TimeMarker* grabbed_marker = _view.get_ruler()->get_grabbed_cursor(); if (grabbed_marker) { @@ -867,7 +1050,7 @@ void Viewport:: mouseMoveEvent(QMouseEvent *event) uint64_t index0 = 0, index1 = 0, index2 = 0; bool logic = false; - for(auto s : _view.session().get_signals()) { + for(auto s : _view.session().get_signals()) { if (mode == LOGIC && s->signal_type() == SR_CHANNEL_LOGIC) { view::LogicSignal *logicSig = (view::LogicSignal*)s; if (logicSig->measure(event->pos(), index0, index1, index2)) { @@ -928,7 +1111,7 @@ void Viewport:: mouseMoveEvent(QMouseEvent *event) hover_x = xrect.right(); } - double rate = (hover_x - xrect.left()) * 1.0 / xrect.width(); + double rate = (hover_x - xrect.left()) * 1.0 / xrect.width(); xc->set_value(xc->grabbed(), min(rate, 1.0)); } else { @@ -936,7 +1119,7 @@ void Viewport:: mouseMoveEvent(QMouseEvent *event) int body_y = _view.get_body_height(); if (msy > body_y) msy = body_y; - + double rate = (msy - xrect.top()) * 1.0 / xrect.height(); xc->set_value(xc->grabbed(), max(rate, 0.0)); } @@ -966,7 +1149,7 @@ void Viewport:: mouseMoveEvent(QMouseEvent *event) _mouse_point = event->pos(); measure(); - + update(UpdateEventType::UPDATE_EV_MS_MOVE); } @@ -983,7 +1166,6 @@ void Viewport::set_action(ActionType action) void Viewport::onLogicMouseRelease(QMouseEvent *event) { bool quickScroll = AppConfig::Instance().appOptions.quickScroll; - bool isMaxWindow = AppControl::Instance()->TopWindowIsMaximized(); switch (_action_type) { @@ -1014,11 +1196,11 @@ void Viewport::onLogicMouseRelease(QMouseEvent *event) if (_mouse_down_point.x() == event->pos().x()) { const auto &sigs = _view.session().get_signals(); - for(auto s : sigs) { + for(auto s : sigs) { if (s->signal_type() == SR_CHANNEL_LOGIC) { view::LogicSignal *logicSig = (view::LogicSignal*)s; if (logicSig->is_by_edge(event->pos(), _edge_start, 10)) { - set_action(LOGIC_JUMP); + set_action(LOGIC_JUMP); _cur_preX = _view.index2pixel(_edge_start); _cur_preY = logicSig->get_y(); _cur_preY_top = logicSig->get_y() - logicSig->get_totalHeight()/2 - 12; @@ -1051,7 +1233,7 @@ void Viewport::onLogicMouseRelease(QMouseEvent *event) } } } - } + } break; } case LOGIC_EDGE: @@ -1085,7 +1267,7 @@ void Viewport::onLogicMouseRelease(QMouseEvent *event) case LOGIC_MOVE: default: break; - } + } } void Viewport::onDsoMouseRelease(QMouseEvent *event) @@ -1129,7 +1311,7 @@ void Viewport::onDsoMouseRelease(QMouseEvent *event) for(auto t : traces){ t->select(false); - } + } } break; } @@ -1177,18 +1359,27 @@ void Viewport::onDsoMouseRelease(QMouseEvent *event) } break; } + default: + break; } } void Viewport::onAnalogMouseRelease(QMouseEvent *event) { - + (void)event; } void Viewport::mouseReleaseEvent(QMouseEvent *event) { assert(event); + // The press was consumed by the y-scale badge; ignore this release so it + // is not also interpreted as a click on the trace below the badge. + if (_yscale_badge_pressed) { + _yscale_badge_pressed = false; + return; + } + if (_type != TIME_VIEW){ update(UpdateEventType::UPDATE_EV_MS_UP); return; @@ -1217,7 +1408,7 @@ void Viewport::mouseReleaseEvent(QMouseEvent *event) set_action(NO_ACTION); auto &xcursor_list = _view.get_xcursorList(); auto i = xcursor_list.begin(); - + while (i != xcursor_list.end()) { (*i)->rel_grabbed(); i++; @@ -1226,7 +1417,7 @@ void Viewport::mouseReleaseEvent(QMouseEvent *event) _xcurs_moved = false; } } - + /* // This code block prevents the cursor from moving. if (mode == LOGIC && event->button() == Qt::LeftButton){ @@ -1290,7 +1481,6 @@ void Viewport::mouseDoubleClickEvent(QMouseEvent *event) index = _view.pixel2index(curX); } - auto &cursor_list = _view.get_cursorList(); _view.add_cursor(index); _view.show_cursors(true); } @@ -1320,7 +1510,6 @@ void Viewport::mouseDoubleClickEvent(QMouseEvent *event) uint64_t index; const double curX = event->pos().x(); index = _view.pixel2index(curX); - auto &cursor_list = _view.get_cursorList(); _view.add_cursor(index); _view.show_cursors(true); } @@ -1340,7 +1529,7 @@ void Viewport::wheelEvent(QWheelEvent *event) bool isVertical = true; #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - x = (int)event->position().x(); + x = (int)event->position().x(); int anglex = event->angleDelta().x(); int angley = event->angleDelta().y(); @@ -1376,7 +1565,7 @@ void Viewport::wheelEvent(QWheelEvent *event) if (_type == FFT_VIEW) { for (auto t : _view.session().get_spectrum_traces()) - { + { if (t->enabled()) { t->zoom(zoom_scale, x); @@ -1390,15 +1579,24 @@ void Viewport::wheelEvent(QWheelEvent *event) if (isVertical) { + const bool vZoom = (event->modifiers() & Qt::ControlModifier) && + (event->modifiers() & Qt::ShiftModifier); + + if (vZoom) { + // Ctrl+Shift+wheel: scale the trace height in the y axis so + // signals can use the full window height. + _view.vzoom(zoom_scale); + flash_yscale_badge(); + } // Vertical scrolling is interpreted as zooming in/out - if(doVScroll) { + else if(doVScroll) { _view.verticalScrollBar()->setValue(_view.verticalScrollBar()->value() - delta); } else { _view.zoom(zoom_scale, x); } } else - { + { bLstTime = false; (void)bLstTime; @@ -1411,7 +1609,7 @@ void Viewport::wheelEvent(QWheelEvent *event) const auto &sigs = _view.session().get_signals(); for (auto s : sigs) { - if (s->signal_type() == SR_CHANNEL_DSO){ + if (s->signal_type() == SR_CHANNEL_DSO){ view::DsoSignal *dsoSig = (view::DsoSignal*)s; dsoSig->auto_end(); } @@ -1499,7 +1697,7 @@ void Viewport::set_receive_len(quint64 length) int mode = _view.session().get_device()->get_work_mode(); if (mode == LOGIC) - { + { if (_view.session().get_device()->is_file() == false) { if (!_is_checked_trig && _view.session().is_triged()){ @@ -1530,14 +1728,14 @@ void Viewport::set_receive_len(quint64 length) if (_view.session().have_new_realtime_refresh(true) == false){ return; } - } + } } if (mode == LOGIC && AppConfig::Instance().appOptions.autoScrollLatestData && _view.session().is_realtime_refresh()) { _view.scroll_to_logic_last_data_time(); - } + } // Received new data, and refresh the view. update(UpdateEventType::UPDATE_EV_GENERIC); @@ -1545,6 +1743,7 @@ void Viewport::set_receive_len(quint64 length) void Viewport::update(int event) { + (void)event; QWidget::update(); } @@ -1570,10 +1769,10 @@ void Viewport::clear_dso_xm() void Viewport::measure() { if (_view.session().is_data_lock()) - return; + return; if (_view.session().is_loop_mode() && _view.session().is_working()) return; - + _measure_type = NO_MEASURE; if (_type == TIME_VIEW) { @@ -1648,26 +1847,31 @@ void Viewport::measure() _edge_hit = false; } } - } + } else if (s->signal_type() == SR_CHANNEL_DSO) { view::DsoSignal *dsoSig = ( view::DsoSignal*)s; - if (s->enabled()) { - if (_measure_en && dsoSig->measure(_view.hover_point())) { - _measure_type = DSO_VALUE; - } - else { - _measure_type = NO_MEASURE; - } + // measure() must run even while the channel is disabled: it + // resets its own hover state (_hover_en = false) and returns + // false in that case. Gating the call itself on s->enabled() + // (as before) skipped that reset entirely, so a channel + // disabled after being hovered kept showing a stale row in + // the floating measurement panel forever. + // + // Only ever promote _measure_type to DSO_VALUE here, never + // reset it back to NO_MEASURE - it already starts each call + // at NO_MEASURE (set once above, before this loop), and with + // several DSO/analog channels in play, one channel failing + // to hover-match (e.g. because it's disabled, or the mouse + // is outside its row in split mode) must not clobber another + // channel's successful match from earlier in this same loop. + if (_measure_en && dsoSig->measure(_view.hover_point())) { + _measure_type = DSO_VALUE; } } else if (s->signal_type() == SR_CHANNEL_ANALOG) { view::AnalogSignal *analogSig = (view::AnalogSignal*)s; - if (s->enabled()) { - if (_measure_en && analogSig->measure(_view.hover_point())) { - _measure_type = DSO_VALUE; - } else { - _measure_type = NO_MEASURE; - } + if (_measure_en && analogSig->measure(_view.hover_point())) { + _measure_type = DSO_VALUE; } } } @@ -1773,6 +1977,17 @@ void Viewport::paintMeasure(QPainter &p, QColor fore, QColor back) } if (_measure_en) { + // Scale the hover info popup together with the traces/text. + const double sc = _view.get_trace_font_scale(); + + // Use an explicit (scaled, non-condensed) font so the popup text + // and its box stay consistent regardless of prior painter state. + QFont measure_font = p.font(); + measure_font.setStretch(QFont::Unstretched); + float mfSize = AppConfig::Instance().GetTraceFontSize(); + measure_font.setPointSizeF(mfSize * sc); + p.setFont(measure_font); + int typical_width = p.boundingRect(0, 0, INT_MAX, INT_MAX, Qt::AlignLeft | Qt::AlignTop, _mm_width_time).width(); typical_width = max(typical_width, p.boundingRect(0, 0, INT_MAX, INT_MAX, @@ -1781,16 +1996,37 @@ void Viewport::paintMeasure(QPainter &p, QColor fore, QColor back) Qt::AlignLeft | Qt::AlignTop, _mm_freq).width()); typical_width = max(typical_width, p.boundingRect(0, 0, INT_MAX, INT_MAX, Qt::AlignLeft | Qt::AlignTop, _mm_duty).width()); - typical_width = typical_width + 100; + + // Measure the (localized) row labels too - a flat padding guess + // isn't enough once translations run longer than English (e.g. + // German "Tastverhältnis: " for "Duty Cycle: "), which is what + // caused the label and value text to overlap in the same row. + const QString label_width_str = L_S(STR_PAGE_DLG, S_ID(IDS_DLG_WIDTH), "Width: "); + const QString label_period_str = L_S(STR_PAGE_DLG, S_ID(IDS_DLG_PERIOD), "Period: "); + const QString label_freq_str = L_S(STR_PAGE_DLG, S_ID(IDS_DLG_FREQUENCY), "Frequency: "); + const QString label_duty_str = L_S(STR_PAGE_DLG, S_ID(IDS_DLG_DUTY_CYCLE), "Duty Cycle: "); + int label_width = p.boundingRect(0, 0, INT_MAX, INT_MAX, + Qt::AlignLeft | Qt::AlignTop, label_width_str).width(); + label_width = max(label_width, p.boundingRect(0, 0, INT_MAX, INT_MAX, + Qt::AlignLeft | Qt::AlignTop, label_period_str).width()); + label_width = max(label_width, p.boundingRect(0, 0, INT_MAX, INT_MAX, + Qt::AlignLeft | Qt::AlignTop, label_freq_str).width()); + label_width = max(label_width, p.boundingRect(0, 0, INT_MAX, INT_MAX, + Qt::AlignLeft | Qt::AlignTop, label_duty_str).width()); + + typical_width = typical_width + label_width + (int)(40 * sc); const QString mm_period_samples_long = _mm_period_samples + " " + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_SAMPLES), " samples"); const QString mm_width_samples_long = _mm_width_samples + " " + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_SAMPLES), " samples"); + const double box_h = 140.0 * sc; + const double pad = 5.0 * sc; + const double row_h = 20.0 * sc; const double width = _view.get_view_width() - _view.verticalScrollBar()->geometry().width(); const double height = _view.get_view_height() - _view.horizontalScrollBar()->geometry().height() - View::StatusHeight; const double left = hoverpoint_x; const double top = hoverpoint_y; const double right = left + typical_width; - const double bottom = top + 140; + const double bottom = top + box_h; double hover_x, hover_y; if(right > width) { hover_x = left - typical_width - MouseEdgeClearance; @@ -1798,7 +2034,7 @@ void Viewport::paintMeasure(QPainter &p, QColor fore, QColor back) hover_x = left + MousePointerClearance; } if(bottom > height) { - hover_y = top - 140 - MousePointerClearance; + hover_y = top - box_h - MousePointerClearance; if(right <= width) { hover_x = left + MouseEdgeClearance; } @@ -1806,41 +2042,41 @@ void Viewport::paintMeasure(QPainter &p, QColor fore, QColor back) hover_y = top + MousePointerClearance; } QPointF org_pos = QPointF(hover_x, hover_y); - QRectF measure_rect = QRectF(org_pos.x(), org_pos.y(), (double)typical_width, 140.0); - QRectF measure1_rect = QRectF(org_pos.x()+5, org_pos.y()+5, (double)typical_width-10, 20.0); - QRectF measure2_rect = QRectF(org_pos.x()+5, org_pos.y()+25, (double)typical_width-10, 20.0); - QRectF measure3_rect = QRectF(org_pos.x()+5, org_pos.y()+50, (double)typical_width-10, 20.0); - QRectF measure4_rect = QRectF(org_pos.x()+5, org_pos.y()+70, (double)typical_width-10, 20.0); - QRectF measure5_rect = QRectF(org_pos.x()+5, org_pos.y()+95, (double)typical_width-10, 20.0); - QRectF measure6_rect = QRectF(org_pos.x()+5, org_pos.y()+115, (double)typical_width-10, 20.0); + const double tw = (double)typical_width - 2 * pad; + QRectF measure_rect = QRectF(org_pos.x(), org_pos.y(), (double)typical_width, box_h); + QRectF measure1_rect = QRectF(org_pos.x()+pad, org_pos.y()+pad, tw, row_h); + QRectF measure2_rect = QRectF(org_pos.x()+pad, org_pos.y()+25.0*sc, tw, row_h); + QRectF measure3_rect = QRectF(org_pos.x()+pad, org_pos.y()+50.0*sc, tw, row_h); + QRectF measure4_rect = QRectF(org_pos.x()+pad, org_pos.y()+70.0*sc, tw, row_h); + QRectF measure5_rect = QRectF(org_pos.x()+pad, org_pos.y()+95.0*sc, tw, row_h); + QRectF measure6_rect = QRectF(org_pos.x()+pad, org_pos.y()+115.0*sc, tw, row_h); p.setPen(Qt::NoPen); p.setBrush(back.black() > 0x80 ? View::TransparentLightBlue : View::TransparentLightYellow); p.drawRect(measure_rect); p.setPen(active_color); - p.drawText(measure1_rect, Qt::AlignLeft | Qt::AlignVCenter, - L_S(STR_PAGE_DLG, S_ID(IDS_DLG_WIDTH), "Width: ")); + p.drawText(measure1_rect, Qt::AlignLeft | Qt::AlignVCenter, label_width_str); p.drawText(measure1_rect, Qt::AlignRight | Qt::AlignVCenter,_mm_width_time); p.drawText(measure2_rect, Qt::AlignRight | Qt::AlignVCenter,mm_width_samples_long); p.setPen(active_color2); - p.drawText(measure3_rect, Qt::AlignLeft | Qt::AlignVCenter, - L_S(STR_PAGE_DLG, S_ID(IDS_DLG_PERIOD), "Period: ")); + p.drawText(measure3_rect, Qt::AlignLeft | Qt::AlignVCenter, label_period_str); p.drawText(measure3_rect, Qt::AlignRight | Qt::AlignVCenter, _mm_period_time); p.drawText(measure4_rect, Qt::AlignRight | Qt::AlignVCenter, mm_period_samples_long); p.setPen(active_color); - p.drawText(measure5_rect, Qt::AlignLeft | Qt::AlignVCenter, - L_S(STR_PAGE_DLG, S_ID(IDS_DLG_FREQUENCY), "Frequency: ")); + p.drawText(measure5_rect, Qt::AlignLeft | Qt::AlignVCenter, label_freq_str); p.drawText(measure5_rect, Qt::AlignRight | Qt::AlignVCenter, _mm_freq); - p.drawText(measure6_rect, Qt::AlignLeft | Qt::AlignVCenter, - L_S(STR_PAGE_DLG, S_ID(IDS_DLG_DUTY_CYCLE), "Duty Cycle: ")); + p.drawText(measure6_rect, Qt::AlignLeft | Qt::AlignVCenter, label_duty_str); p.drawText(measure6_rect, Qt::AlignRight | Qt::AlignVCenter, _mm_duty); } - } + } if (_action_type == NO_ACTION && _measure_type == DSO_VALUE) { + struct MRow { QString name; QString val; QColor colour; }; + std::vector rows; + for(auto s : _view.session().get_signals()) { if (s->signal_type() == SR_CHANNEL_DSO) { uint64_t index; @@ -1852,8 +2088,14 @@ void Viewport::paintMeasure(QPainter &p, QColor fore, QColor back) p.setBrush(Qt::NoBrush); p.drawLine(hpoint.x(), dsoSig->get_view_rect().top(), hpoint.x(), dsoSig->get_view_rect().bottom()); + + MRow r; + r.name = "CH" + dsoSig->get_name(); + r.val = dsoSig->get_voltage(dsoSig->get_hw_offset() - value, 3); + r.colour = dsoSig->get_colour(); + rows.push_back(r); } - } + } else if (s->signal_type() == SR_CHANNEL_ANALOG) { uint64_t index; double value; @@ -1867,10 +2109,64 @@ void Viewport::paintMeasure(QPainter &p, QColor fore, QColor back) } } } + + // Draw the hovered voltages in a single floating panel (like the + // logic-mode measurement popup) instead of printing each number on + // top of its trace, where it is hard to read. + if (!rows.empty()) { + // The floating panel is drawn 50% larger than the trace font and + // in bold to keep the hovered values easy to read. + const double sc = _view.get_trace_font_scale() * 1.2; + QFont measure_font = p.font(); + measure_font.setStretch(QFont::Unstretched); + measure_font.setBold(true); + measure_font.setPointSizeF(AppConfig::Instance().GetTraceFontSize() * sc); + p.setFont(measure_font); + + int name_w = 0, val_w = 0; + for (auto &r : rows) { + name_w = max(name_w, p.boundingRect(0, 0, INT_MAX, INT_MAX, + Qt::AlignLeft | Qt::AlignVCenter, r.name).width()); + val_w = max(val_w, p.boundingRect(0, 0, INT_MAX, INT_MAX, + Qt::AlignLeft | Qt::AlignVCenter, r.val).width()); + } + + const double pad = 6.0 * sc; + const double gap = 16.0 * sc; + const double row_h = 20.0 * sc; + const double box_w = pad * 2 + name_w + gap + val_w; + const double box_h = pad * 2 + row_h * rows.size(); + + const double width = _view.get_view_width() + - _view.verticalScrollBar()->geometry().width(); + const double vheight = _view.get_view_height() + - _view.horizontalScrollBar()->geometry().height() - View::StatusHeight; + + // Offset from the cursor, flipping to stay inside the view. + double bx = hoverpoint_x + MousePointerClearance; + double by = hoverpoint_y + MousePointerClearance; + if (bx + box_w > width) bx = hoverpoint_x - box_w - MouseEdgeClearance; + if (by + box_h > vheight) by = hoverpoint_y - box_h - MousePointerClearance; + if (bx < 0) bx = 0; + if (by < 0) by = 0; + + p.setPen(Qt::NoPen); + p.setBrush(back.black() > 0x80 ? View::TransparentLightBlue + : View::TransparentLightYellow); + p.drawRect(QRectF(bx, by, box_w, box_h)); + + for (size_t i = 0; i < rows.size(); i++) { + QRectF rr(bx + pad, by + pad + i * row_h, box_w - 2 * pad, row_h); + p.setPen(rows[i].colour); + p.drawText(rr, Qt::AlignLeft | Qt::AlignVCenter, rows[i].name); + p.setPen(fore); + p.drawText(rr, Qt::AlignRight | Qt::AlignVCenter, rows[i].val); + } + } } if (_dso_ym_valid) { - for(auto s : _view.session().get_signals()) { + for(auto s : _view.session().get_signals()) { if (s->signal_type() == SR_CHANNEL_DSO) { view::DsoSignal *dsoSig = (view::DsoSignal*)s; if (dsoSig->get_index() == _dso_ym_sig_index) { @@ -1989,6 +2285,7 @@ void Viewport::paintMeasure(QPainter &p, QColor fore, QColor back) measure_line_count += 3; } p.drawLines(measure_lines, measure_line_count); + delete[] measure_lines; if (dso_xm_stage < DsoMeasureStages) { p.drawLine(x[dso_xm_stage-1], _dso_xm_y, _mouse_point.x(), _dso_xm_y); @@ -1998,7 +2295,7 @@ void Viewport::paintMeasure(QPainter &p, QColor fore, QColor back) measure_updated(); } - if (_action_type == LOGIC_EDGE + if (_action_type == LOGIC_EDGE && _view.session().have_view_data()){ p.setPen(active_color); p.drawLine(QLineF(_cur_preX, _cur_midY-5, _cur_preX, _cur_midY+5)); @@ -2072,10 +2369,10 @@ void Viewport::paintMeasure(QPainter &p, QColor fore, QColor back) QString delta_text = _view.get_index_delta(_edge_start, _edge_end) + "/" + QString::number(delta); QFontMetrics fm = this->fontMetrics(); - + const int rectW = fm.boundingRect(delta_text).width() + 60; const int rectH = fm.height() + 10; - + const int rectY = (height() - hoverpoint_x < rectH + 20) ? hoverpoint_y - 10 - rectH : hoverpoint_y + 20; const int rectX = (width() - hoverpoint_x < rectW) ? hoverpoint_x - rectW : hoverpoint_x; QRectF jump_rect = QRectF(rectX, rectY, rectW, rectH); @@ -2162,14 +2459,14 @@ void Viewport::on_trigger_timer() } void Viewport::on_drag_timer() -{ +{ const int64_t offset = _view.x_offset(); const double scale = _view.scale(); if (_view.session().is_stopped_status() && _drag_strength != 0 && offset < _view.get_max_offset() - && offset > _view.get_min_offset()) + && offset > _view.get_min_offset()) { _view.set_scale_offset(scale, offset + _drag_strength); _drag_strength /= DragDamping; @@ -2205,7 +2502,7 @@ void Viewport::show_wait_trigger() } void Viewport::unshow_wait_trigger() -{ +{ _waiting_trig = 0; update(UpdateEventType::UPDATE_EV_GENERIC); } @@ -2217,6 +2514,8 @@ bool Viewport::get_dso_trig_moved() void Viewport::show_contextmenu(const QPoint& pos) { + // The X/Y cursor menu is a DSO-only concept; logic mode has no context + // menu (the y-scale is reset by clicking the badge, see paintYScaleBadge). if(_cmenu && _view.session().get_device()->get_work_mode() == DSO) { @@ -2226,17 +2525,49 @@ void Viewport::show_contextmenu(const QPoint& pos) } } +void Viewport::reset_yscale() +{ + _view.set_trace_height_factor(1.0); + flash_yscale_badge(); +} + +void Viewport::on_yscale_hint_timeout() +{ + // Keep the badge fully visible while the pointer hovers over it; the + // fade only (re)starts once the mouse leaves the badge. + const bool hovered = !_yscale_badge_rect.isEmpty() && underMouse() + && _yscale_badge_rect.contains(mapFromGlobal(QCursor::pos())); + + if (hovered) { + _yscale_hint_clock.restart(); + } + else if (_yscale_hint_clock.elapsed() >= YScaleBadgeDurationMs) { + _yscale_hint_active = false; + _yscale_hint_timer.stop(); + _yscale_badge_rect = QRect(); + } + QWidget::update(); +} + +void Viewport::flash_yscale_badge() +{ + _yscale_hint_active = true; + _yscale_hint_clock.restart(); + _yscale_hint_timer.start(YScaleBadgeTickMs); + QWidget::update(); +} + void Viewport::add_cursor_y() { uint64_t index; - index = _view.pixel2index(_cur_preX); + index = _view.pixel2index(_cur_preX); _view.add_cursor(index); _view.show_cursors(true); } void Viewport::add_cursor_x() { - double ypos = (_cur_preY - _view.get_view_rect().top()) * 1.0 / _view.get_view_height(); + double ypos = (_cur_preY - _view.get_view_rect().top()) * 1.0 / _view.get_view_height(); _view.add_xcursor(ypos, ypos); _view.show_xcursors(true); } @@ -2252,7 +2583,7 @@ void Viewport::UpdateTheme() } void Viewport::UpdateFont() -{ +{ QFont font = this->font(); font.setPointSizeF(AppConfig::Instance().appOptions.fontSize); _yAction->setFont(font); diff --git a/DSView/pv/view/viewport.h b/DSView/pv/view/viewport.h index 1a70e449c..9c612f099 100644 --- a/DSView/pv/view/viewport.h +++ b/DSView/pv/view/viewport.h @@ -4,6 +4,7 @@ * * Copyright (C) 2012 Joel Holdsworth * Copyright (C) 2013 DreamSourceLab + * Copyright (C) 2026 Schildkroet * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -31,6 +32,8 @@ #include #include #include +#include +#include #include #include "../view/view.h" @@ -80,6 +83,9 @@ class Viewport : public QWidget, public IUiWindow static const int WaitLoopTime = 400; static const int MousePointerClearance = 25; static const int MouseEdgeClearance = 3; + static const int YScaleBadgeDurationMs = 4000; // total on-screen time + static const int YScaleBadgeFadeMs = 800; // fade-out tail duration + static const int YScaleBadgeTickMs = 40; // repaint interval while fading enum ActionType { NO_ACTION, @@ -149,6 +155,11 @@ class Viewport : public QWidget, public IUiWindow void paintProgress(QPainter& p, QColor fore, QColor back); void paintMeasure(QPainter &p, QColor fore, QColor back); void paintCursors(QPainter &p); + void paint_ref_waves(QPainter &p); + void paintYScaleBadge(QPainter &p, QColor fore, QColor back); + + // Briefly show the y-scale badge (logic mode) after a vertical zoom. + void flash_yscale_badge(); void start_trigger_timer(int msec); void get_captured_progress(double &progress, int &progress100); @@ -167,6 +178,8 @@ private slots: void show_contextmenu(const QPoint& pos); void add_cursor_x(); void add_cursor_y(); + void reset_yscale(); + void on_yscale_hint_timeout(); signals: void measure_updated(); @@ -179,6 +192,12 @@ private slots: QPixmap _pixmap; QMenu *_cmenu; + // Reusable scratch buffer for paint_ref_waves(), so it does not have to + // heap-allocate/free a QPointF array on every repaint (including ones + // triggered by mere mouse movement) - QVector::resize() only reallocates + // when growing past the buffer's current capacity. + QVector _ref_wave_points; + uint64_t _sample_received; QPoint _mouse_point; QPoint _mouse_down_point; @@ -186,6 +205,7 @@ private slots: double _curScale; int64_t _curOffset; int _curSignalHeight; + int64_t _curYOffset; bool _measure_en; ActionType _action_type; @@ -255,6 +275,12 @@ private slots: int _tigger_wait_times; QAction *_yAction; QAction *_xAction; + + QTimer _yscale_hint_timer; // repaint tick while the badge fades + QElapsedTimer _yscale_hint_clock; // time since the badge was last shown + bool _yscale_hint_active; + QRect _yscale_badge_rect; // clickable reset hit-area (empty = hidden) + bool _yscale_badge_pressed; // press consumed by the badge; swallow release }; } // namespace view diff --git a/DSView/pv/winnativewidget.cpp b/DSView/pv/winnativewidget.cpp index c6a2aa509..8d976b7ec 100644 --- a/DSView/pv/winnativewidget.cpp +++ b/DSView/pv/winnativewidget.cpp @@ -23,7 +23,6 @@ #include "winnativewidget.h" #include -#include #include #include #include @@ -200,7 +199,12 @@ WinNativeWidget::~WinNativeWidget() if (_hWnd){ Show(false); DestroyWindow(_hWnd); - } + } + + if (_shadow != NULL){ + delete _shadow; + _shadow = NULL; + } } void WinNativeWidget::SetChildWidget(MainFrame *w) @@ -254,16 +258,20 @@ LRESULT CALLBACK WinNativeWidget::WndProc(HWND hWnd, UINT message, WPARAM wParam break; } case WM_KEYDOWN: - { + { //enable the hot key. - QKeyEvent keyEvent(QEvent::KeyPress, (int)wParam, 0); - QApplication::sendEvent(self->_childWidget->GetBodyView(), &keyEvent); + if (self->_childWidget != NULL){ + QKeyEvent keyEvent(QEvent::KeyPress, (int)wParam, Qt::NoModifier); + QApplication::sendEvent(self->_childWidget->GetBodyView(), &keyEvent); + } break; } case WM_KEYUP: - { - QKeyEvent keyEvent(QEvent::KeyRelease, (int)wParam, 0); - QApplication::sendEvent(self->_childWidget->GetBodyView(), &keyEvent); + { + if (self->_childWidget != NULL){ + QKeyEvent keyEvent(QEvent::KeyRelease, (int)wParam, Qt::NoModifier); + QApplication::sendEvent(self->_childWidget->GetBodyView(), &keyEvent); + } break; } case WM_ENTERSIZEMOVE: diff --git a/DSView/pv/winnativewidget.h b/DSView/pv/winnativewidget.h index 18c12c8e3..5d0f2c191 100644 --- a/DSView/pv/winnativewidget.h +++ b/DSView/pv/winnativewidget.h @@ -24,7 +24,9 @@ #ifndef WINNATIVEWINDOW_H #define WINNATIVEWINDOW_H +#ifndef UNICODE #define UNICODE +#endif #include #include diff --git a/DSView/pv/winshadow.cpp b/DSView/pv/winshadow.cpp index 8b806d766..d72a01000 100644 --- a/DSView/pv/winshadow.cpp +++ b/DSView/pv/winshadow.cpp @@ -108,8 +108,8 @@ void WinShadow::hideShadow() QWidget::hide(); } -bool WinShadow::nativeEvent(const QByteArray &eventType, void *message, long *result) -{ +bool WinShadow::nativeEvent(const QByteArray &eventType, void *message, SHADOW_MESSAGE_RESULT_PTR result) +{ MSG *msg = static_cast(message); switch (msg->message) @@ -139,7 +139,7 @@ bool WinShadow::nativeEvent(const QByteArray &eventType, void *message, long *re case WM_NCLBUTTONDBLCLK: case WM_NCHITTEST: { - *result = long(SendMessageW(m_hwnd, msg->message, msg->wParam, msg->lParam)); + *result = (SHADOW_MESSAGE_RESULT_TYPE)SendMessageW(m_hwnd, msg->message, msg->wParam, msg->lParam); return true; } } diff --git a/DSView/pv/winshadow.h b/DSView/pv/winshadow.h index b6bc9108d..b9f4ad5f2 100644 --- a/DSView/pv/winshadow.h +++ b/DSView/pv/winshadow.h @@ -29,6 +29,14 @@ #define SHADOW_BORDER_WIDTH 11 +#if QT_VERSION >= QT_VERSION_CHECK(6,0,0) +typedef qintptr *SHADOW_MESSAGE_RESULT_PTR; +typedef qintptr SHADOW_MESSAGE_RESULT_TYPE; +#else +typedef long *SHADOW_MESSAGE_RESULT_PTR; +typedef long SHADOW_MESSAGE_RESULT_TYPE; +#endif + namespace pv { class IShadowCallback @@ -69,7 +77,7 @@ private slots: void onCheckForeWindow(); private: - bool nativeEvent(const QByteArray &eventType, void *message, long *result) override; + bool nativeEvent(const QByteArray &eventType, void *message, SHADOW_MESSAGE_RESULT_PTR result) override; void paintEvent(QPaintEvent *event) override; QWidget *m_parent; diff --git a/DSView/themes/breeze.qrc b/DSView/themes/breeze.qrc index 620630a51..3ec5c203e 100644 --- a/DSView/themes/breeze.qrc +++ b/DSView/themes/breeze.qrc @@ -62,6 +62,8 @@ dark/undock-hover.svg light.qss dark.qss + latte.qss + frappe.qss light/checkbox_checked.svg light/checkbox_checked_disabled.svg light/checkbox_checked-hover.svg diff --git a/DSView/themes/frappe.qss b/DSView/themes/frappe.qss new file mode 100644 index 000000000..98cced8cf --- /dev/null +++ b/DSView/themes/frappe.qss @@ -0,0 +1,1662 @@ +/* + * Catppuccin Frappe theme for DSView + * Generated from dark.qss via a systematic + * Catppuccin color substitution - see gen_theme.py in this session's + * scratchpad for the exact mapping used. + */ +/* + * DSView dark stylesheet. + * --------------------------------------------------------------------- + * The MIT License (MIT) + * + * Copyright (c) <2013-2014> + * Copyright (C) 2019 DreamSourceLab + * + * 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. + * --------------------------------------------------------------------- + */ + +QToolTip +{ + border: 1px solid #c6d0f5; + background-color: #303446; + alternate-background-color: #414559; + color: #c6d0f5; + padding: 1px; + opacity: 200; +} + +QWidget +{ + color: #c6d0f5; + background-color: #303446; + selection-background-color:#8caaee; + selection-color: #c6d0f5; + background-clip: border; + image: none; + border: 0px transparent; + outline: 0; +} + +/* +QWidget:item:hover +{ + background-color: #8caaee; + color: #c6d0f5; +} + +QWidget:item:selected +{ + background-color: #8caaee; +} +*/ + +QPushButton#flat{ + text-align:left; + border:none; +} + +QPushButton#flat:hover +{ + background-color: #85c1dc; + color: #ca9ee6; +} + +QCheckBox +{ + spacing: 0px; + outline: none; + color: #c6d0f5; + margin-bottom: 2px; + opacity: 200; +} + +QCheckBox:disabled +{ + color: #414559; +} + +QGroupBox::indicator +{ + width: 18px; + height: 18px; + margin-left: 2px; +} + +QCheckBox::indicator:unchecked, +QCheckBox::indicator:unchecked:focus +{ + image: url(:/dark/checkbox_unchecked.svg); +} + +QCheckBox::indicator:unchecked:hover, +QCheckBox::indicator:unchecked:pressed, +QGroupBox::indicator:unchecked:hover, +QGroupBox::indicator:unchecked:focus, +QGroupBox::indicator:unchecked:pressed +{ + border: none; + image: url(:/dark/checkbox_unchecked-hover.svg); +} + +QCheckBox::indicator:checked +{ + image: url(:/dark/checkbox_checked.svg); +} + +QCheckBox::indicator:checked:hover, +QCheckBox::indicator:checked:focus, +QCheckBox::indicator:checked:pressed, +QGroupBox::indicator:checked:hover, +QGroupBox::indicator:checked:focus, +QGroupBox::indicator:checked:pressed +{ + border: none; + image: url(:/dark/checkbox_checked-hover.svg); +} + +QCheckBox::indicator:indeterminate +{ + image: url(:/dark/checkbox_indeterminate.svg); +} + +QCheckBox::indicator:indeterminate:focus, +QCheckBox::indicator:indeterminate:hover +QCheckBox::indicator:indeterminate:pressed +{ + image: url(:/dark/checkbox_indeterminate-hover.svg); +} + +QCheckBox::indicator:indeterminate:disabled +{ + image: url(:/dark/checkbox_indeterminate_disabled.svg); +} + +QCheckBox::indicator:checked:disabled, +QGroupBox::indicator:checked:disabled +{ + image: url(:/dark/checkbox_checked_disabled.svg); +} + +QCheckBox::indicator:unchecked:disabled, +QGroupBox::indicator:unchecked:disabled +{ + image: url(:/dark/checkbox_unchecked_disabled.svg); +} + +QRadioButton +{ + spacing: 5px; + outline: none; + color: #c6d0f5; + margin-bottom: 2px; +} + +QRadioButton:disabled +{ + color: #414559; +} +QRadioButton::indicator +{ + width: 16px; + height: 16px; +} + +QRadioButton::indicator:unchecked, +QRadioButton::indicator:unchecked:focus +{ + image: url(:/dark/radio_unchecked.svg); +} + + +QRadioButton::indicator:unchecked:hover, +QRadioButton::indicator:unchecked:pressed +{ + border: none; + outline: none; + image: url(:/dark/radio_unchecked-hover.svg); +} + + +QRadioButton::indicator:checked +{ + border: none; + outline: none; + image: url(:/dark/radio_checked.svg); +} + +QRadioButton::indicator:checked:hover, +QRadioButton::indicator:checked:focus, +QRadioButton::indicator:checked:pressed +{ + border: none; + outline: none; + image: url(:/dark/radio_checked-hover.svg); +} + +QRadioButton::indicator:checked:disabled +{ + outline: none; + image: url(:/dark/radio_checked_disabled.svg); +} + +QRadioButton::indicator:unchecked:disabled +{ + image: url(:/dark/radio_unchecked_disabled.svg); +} + +QMenuBar +{ + background-color: #303446; + color: #c6d0f5; +} + +QMenuBar::item +{ + background: transparent; +} + +QMenuBar::item:selected +{ + background: transparent; + border: 1px transparent; +} + +QMenuBar::item:pressed +{ + border: 1px transparent; + background-color: #8caaee; + color: #c6d0f5; + margin-bottom: -1px; + padding-bottom: 1px; +} + +QMenu +{ + border: 1px transparent; + color: #c6d0f5; + margin: 0px; +} + +QMenu::item +{ + padding: 5px 30px 5px 30px; + margin-left: 2px; + border: 1px solid transparent; /* reserve space for selection border */ +} + +QMenu::item:selected +{ + background-color: #8caaee; + color: #c6d0f5; +} + +QMenu::separator +{ + height: 2px; + background: lightblue; + margin-left: 10px; + margin-right: 5px; +} + +QMenu::indicator { + width: 18px; + height: 18px; +} + +/* non-exclusive indicator = check box style indicator + (see QActionGroup::setExclusive) */ +QMenu::indicator:non-exclusive:unchecked +{ + image: url(:/dark/checkbox_unchecked_disabled.svg); +} + +QMenu::indicator:non-exclusive:unchecked:selected +{ + image: url(:/dark/checkbox_unchecked_disabled.svg); +} + +QMenu::indicator:non-exclusive:checked +{ + image: url(:/dark/checkbox_checked.svg); +} + +QMenu::indicator:non-exclusive:checked:selected +{ + image: url(:/dark/checkbox_checked.svg); +} + +/* exclusive indicator = radio button style indicator (see QActionGroup::setExclusive) */ +QMenu::indicator:exclusive:unchecked +{ + image: url(:/dark/radio_unchecked_disabled.svg); +} + +QMenu::indicator:exclusive:unchecked:selected +{ + image: url(:/dark/radio_unchecked_disabled.svg); +} + +QMenu::indicator:exclusive:checked +{ + image: url(:/dark/radio_checked.svg); +} + +QMenu::indicator:exclusive:checked:selected +{ + image: url(:/dark/radio_checked.svg); +} + +QMenu::right-arrow +{ + margin: 5px; + image: url(:/dark/right_arrow.svg); +} + + +QWidget:disabled +{ + color: #414559; + background-color: #303446; +} + +QAbstractItemView +{ + alternate-background-color: #414559; + color: #c6d0f5; + border: 1px transparent; + border-radius: 2px; + padding: 1px +} + +QTabWidget:focus, +QCheckBox:focus, +QRadioButton:focus, +QSlider:focus +{ + border: none; +} + +QLineEdit +{ + background-color: #232634; + padding: 2px; + border-style: solid; + border: 1px solid #414559; + border-radius: 2px; + color: #c6d0f5; +} + +QTextEdit +{ + background-color: #232634; + padding: 2px; + border-style: solid; + border: 1px solid #414559; + border-radius: 2px; + color: #c6d0f5; +} + +QGroupBox +{ + border: 1px solid #414559; + border-radius: 2px; + margin-top: 20px; +} + +QGroupBox::title +{ + subcontrol-origin: margin; + subcontrol-position: top center; + padding-left: 10px; + padding-right: 10px; + padding-top: 10px; +} + +QScrollBar:horizontal +{ + height: 24px; + margin: 3px 12px 3px 12px; + border: 1px transparent; + border-radius: 9px; + background-color: #232634; +} + +QScrollBar::handle:horizontal +{ + background-color: #737994; + min-width: 20px; + border-radius: 9px; +} + +QScrollBar::add-line:horizontal +{ + margin: 0px 3px 0px 3px; + image: url(:/dark/right_arrow_disabled.svg); + width: 10px; + height: 10px; + subcontrol-position: right; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:horizontal +{ + margin: 0px 3px 0px 3px; + image: url(:/dark/left_arrow_disabled.svg); + width: 10px; + height: 10px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::add-line:horizontal:hover, +QScrollBar::add-line:horizontal:on +{ + image: url(:/dark/right_arrow.svg); + width: 10px; + height: 10px; + subcontrol-position: right; + subcontrol-origin: margin; +} + + +QScrollBar::sub-line:horizontal:hover, +QScrollBar::sub-line:horizontal:on +{ + image: url(:/dark/left_arrow.svg); + width: 10px; + height: 10px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:horizontal, +QScrollBar::down-arrow:horizontal +{ + background: none; +} + + +QScrollBar::add-page:horizontal, +QScrollBar::sub-page:horizontal +{ + background: none; +} + +QScrollBar:vertical +{ + background-color: #232634; + width: 24px; + margin: 12px 3px 12px 3px; + border: 1px transparent; + border-radius: 9px; +} + +QScrollBar::handle:vertical +{ + background-color: #737994; + min-height: 20px; + border-radius: 9px; +} + +QScrollBar::sub-line:vertical +{ + margin: 3px 0px 3px 0px; + image: url(:/dark/up_arrow_disabled.svg); + height: 10px; + width: 10px; + subcontrol-position: top; + subcontrol-origin: margin; +} + +QScrollBar::add-line:vertical +{ + margin: 3px 0px 3px 0px; + image: url(:/dark/down_arrow_disabled.svg); + height: 10px; + width: 10px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:vertical:hover, +QScrollBar::sub-line:vertical:on +{ + + image: url(:/dark/up_arrow.svg); + height: 10px; + width: 10px; + subcontrol-position: top; + subcontrol-origin: margin; +} + + +QScrollBar::add-line:vertical:hover, +QScrollBar::add-line:vertical:on +{ + image: url(:/dark/down_arrow.svg); + height: 10px; + width: 10px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:vertical, +QScrollBar::down-arrow:vertical +{ + background: none; +} + + +QScrollBar::add-page:vertical, +QScrollBar::sub-page:vertical +{ + background: none; +} + +QTextEdit +{ + background-color: #232634; + color: #c6d0f5; + border: 1px solid #414559; + margin: 0; +} + +QPlainTextEdit +{ + background-color: #232634;; + color: #c6d0f5; + border-radius: 2px; + border: 1px solid #414559; +} + +QHeaderView::section +{ + background-color: #737994; + color: #c6d0f5; + padding-left: 4px; + border: 1px solid #737994; +} + +QSizeGrip +{ + image: url(:/dark/sizegrip.svg); + width: 12px; + height: 12px; +} + +QMenu::separator +{ + height: 1px; + background-color: #737994; + color: white; + padding-left: 4px; + margin-left: 10px; + margin-right: 5px; +} + +QFrame +{ + border-radius: 2px; + border: 1px solid #414559; +} + +QFrame[frameShape="0"] +{ + border-radius: 2px; + border: 1px transparent; +} + +QStackedWidget +{ + border: 1px transparent; +} + +QToolBar +{ + border: 1px transparent; + background: 1px solid #303446; + padding: 0px; +} + +QToolBar::handle:horizontal +{ + image: url(:/dark/hmovetoolbar.svg); + width = 16px; + height = 64px; +} + +QToolBar::handle:vertical +{ + image: url(:/dark/vmovetoolbar.svg); + width = 54px; + height = 10px; +} + +QToolBar::separator:horizontal +{ + image: url(:/dark/hsepartoolbar.svg); + width = 7px; + height = 63px; +} + +QToolBar::separator:vertical +{ + image: url(:/dark/vsepartoolbars.svg); + width = 63px; + height = 7px; +} + +QPushButton +{ + color: #c6d0f5; + background-color: #303446; + border-width: 1px; + border-color: #232634; + border-style: solid; + padding: 5px; + border-radius: 5px; + outline: none; +} + +QPushButton:disabled +{ + background-color: #303446; + border-width: 1px; + border-color: #414559; + border-style: solid; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 10px; + padding-right: 10px; + border-radius: 5px; + color: #414559; +} + +QPushButton:focus +{ + color: white; +} + +QPushButton:pressed +{ + background-color: #303446; + padding-top: -15px; + padding-bottom: -17px; +} + + +QPushButton:checked{ + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #303446, + stop: 0.5 #737994, + stop: 1 #303446); +} + +QPushButton:hover +{ + background-color: #303446; + border: 1px solid #8caaee; + color: #c6d0f5; + padding-top: 6px; + padding-bottom: 4px; +} + +QPushButton:checked:hover +{ + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #303446, + stop: 0.5 #737994, + stop: 1 #303446); + border: 1px solid #8caaee; + color: #c6d0f5; + padding-top: 6px; + padding-bottom: 4px; +} + +QComboBox:hover, +QAbstractSpinBox:hover, +QLineEdit:hover, +QTextEdit:hover, +QPlainTextEdit:hover, +QAbstractView:hover, +QTreeView:hover +{ + border: 1px solid #8caaee; + color: #c6d0f5; +} + +QComboBox:on +{ + background-color: #626880; + padding-top: 3px; + padding-left: 4px; + selection-background-color: #51576d; +} + +QComboBox +{ + selection-background-color: #85c1dc; + background-color: #232634; + border-style: solid; + border: 1px solid #414559; + border-radius: 2px; + padding: 2px; + min-width: 30px; +} + + +QComboBox::drop-down +{ + subcontrol-origin: padding; + subcontrol-position: top right; + width: 10px; + + border-left-width: 0px; + border-left-color: darkgray; + border-left-style: solid; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +QComboBox::down-arrow +{ + image: url(:/dark/down_arrow_disabled.svg); +} + +QComboBox::down-arrow:on, +QComboBox::down-arrow:hover, +QComboBox::down-arrow:focus +{ + image: url(:/dark/down_arrow.svg); +} + +QAbstractSpinBox +{ + padding: 2px; + border: 1px solid #737994; + background-color: #232634; + color: #c6d0f5; + border-radius: 2px; + min-width: 60px; +} + +QAbstractSpinBox:up-button +{ + background-color: transparent; + subcontrol-origin: border; + subcontrol-position: center right; +} + +QAbstractSpinBox:down-button +{ + background-color: transparent; + subcontrol-origin: border; + subcontrol-position: center left; +} + +QAbstractSpinBox::up-arrow, +QAbstractSpinBox::up-arrow:disabled, +QAbstractSpinBox::up-arrow:off +{ + image: url(:/dark/up_arrow_disabled.svg); + width: 10px; + height: 10px; +} + +QAbstractSpinBox::up-arrow:hover +{ + image: url(:/dark/up_arrow.svg); +} + +QAbstractSpinBox::down-arrow, +QAbstractSpinBox::down-arrow:disabled, +QAbstractSpinBox::down-arrow:off +{ + image: url(:/dark/down_arrow_disabled.svg); + width: 10px; + height: 10px; +} + +QAbstractSpinBox::down-arrow:hover +{ + image: url(:/dark/down_arrow.svg); +} + +QLabel +{ + border: 0px solid black; + margin-left: 2px; + margin-right: 2px; +} + +/* BORDERS */ +QTabWidget::pane +{ + padding: 0px; + padding-right: 3px; + margin: 0px; +} + +QTabWidget::pane:top +{ + border: 1px transparent; + top: -1px; +} + +QTabWidget::pane:bottom +{ + border: 1px transparent; + bottom: -1px; +} + +QTabWidget::pane:left +{ + border: 1px transparent; + right: -1px; +} + +QTabWidget::pane:right +{ + border: 1px transparent; + left: -1px; +} + + +QTabBar +{ + qproperty-drawBase: 0; + margin: 0px; + padding: 0px; + border-radius: 3px; +} + +QTabBar:focus +{ + border: 0px transparent; +} + +QTabBar::close-button +{ + image: url(:/dark/close.svg); + background: transparent; +} + +QTabBar::close-button:hover +{ + image: url(:/dark/close-hover.svg); + width: 12px; + height: 12px; + background: transparent; +} + +QTabBar::close-button:pressed +{ + image: url(:/dark/close-pressed.svg); + width: 12px; + height: 12px; + background: transparent; +} + +QTabBar::tab +{ + color: white; + background-color: #85c1dc; + padding: 5px; +} + +QTabBar::tab:!selected +{ + color: white; + background-color: #303446; +} + +QTabBar::tab:disabled +{ + color: #414559; + background-color: #303446; +} + +/* TOP TABS */ +QTabBar::tab:top +{ + border: 1px solid #51576d; + border-bottom: 1px transparent; + border-top-left-radius: 2px; + border-top-right-radius: 2px; +} + +QTabBar::tab:top:!selected +{ + border: 1px solid #51576d; + border-bottom: 1px transparent; + border-top-left-radius: 2px; + border-top-right-radius: 2px; +} + +QTabBar::tab:top:hover +{ + border: 1px solid #8caaee; + border-bottom: 1px transparent; +} + + +/* BOTTOM TABS */ +QTabBar::tab:bottom +{ + border: 1px solid #51576d; + border-top: 1px transparent; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} + +QTabBar::tab:bottom:!selected +{ + border: 1px solid #51576d; + border-top: 1px transparent; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} + +QTabBar::tab:bottom:hover +{ + border: 1px solid #8caaee; + border-top: 1px transparent; +} + + +/* LEFT TABS */ +QTabBar::tab:left +{ + border: 1px solid #51576d; + border-left: 1px transparent; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; +} + +QTabBar::tab:left:!selected +{ + border: 1px solid #51576d; + border-left: 1px transparent; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; +} + +QTabBar::tab:left:hover +{ + border: 1px solid #8caaee; + border-left: 1px transparent; +} + + +/* RIGHT TABS */ +QTabBar::tab:right +{ + border: 1px solid #51576d; + border-right: 1px transparent; + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} + +QTabBar::tab:right:!selected +{ + border: 1px solid #51576d; + border-right: 1px transparent; + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} + +QTabBar::tab:right:hover +{ + border: 1px solid #8caaee; + border-right: 1px transparent; +} + + +QTabBar QToolButton::right-arrow:enabled +{ + image: url(:/dark/right_arrow.svg); +} + +QTabBar QToolButton::left-arrow:enabled +{ + image: url(:/dark/left_arrow.svg); +} + +QTabBar QToolButton::right-arrow:disabled +{ + image: url(:/dark/right_arrow_disabled.svg); +} + +QTabBar QToolButton::left-arrow:disabled +{ + image: url(:/dark/left_arrow_disabled.svg); +} + +QDockWidget +{ + background: #303446; + border: 1px transparent; + titlebar-close-icon: url(:/dark/transparent.svg); + titlebar-normal-icon: url(:/dark/transparent.svg); +} + +QDockWidget::title { + background-color: #232634; + padding-top: 7px; + border: 1px solid #292c3c; +} + +QDockWidget::close-button, +QDockWidget::float-button +{ + border: 1px solid transparent; + border-radius: 2px; + background: transparent; +} + +QDockWidget::float-button +{ + image: url(:/dark/undock.svg); +} + +QDockWidget::float-button:hover +{ + image: url(:/dark/undock-hover.svg) ; +} + +QDockWidget::close-button +{ + image: url(:/dark/close.svg) ; +} + +QDockWidget::close-button:hover +{ + image: url(:/dark/close-hover.svg) ; +} + +QDockWidget::close-button:pressed +{ + image: url(:/dark/close-pressed.svg) ; +} + +QTreeView, +QListView +{ + border: 1px solid #737994; + background-color: #303446; +} + +QTreeView::branch:has-siblings:!adjoins-item +{ + image: url(:/dark/stylesheet-vline.svg) 0; +} + +QTreeView::branch:has-siblings:adjoins-item +{ + image: url(:/dark/stylesheet-branch-more.svg) 0; +} + +QTreeView::branch:!has-children:!has-siblings:adjoins-item +{ + image: url(:/dark/stylesheet-branch-end.svg) 0; +} + +QTreeView::branch:has-children:!has-siblings:closed, +QTreeView::branch:closed:has-children:has-siblings +{ + image: url(:/dark/stylesheet-branch-end-closed.svg) 0; + image: url(:/dark/branch_closed.svg); +} + +QTreeView::branch:open:has-children:!has-siblings, +QTreeView::branch:open:has-children:has-siblings +{ + image: url(:/dark/stylesheet-branch-end-open.svg) 0; + image: url(:/dark/branch_open.svg); +} + + +QSlider::groove:horizontal +{ + border: 1px solid #303446; + height: 4px; + background: #838ba7; + margin: 0px; + border-radius: 1px; +} + +QSlider::handle:horizontal +{ + background: #303446; + border: 1px solid #8caaee; + width: 16px; + margin: -7px 0; + border-radius: 8px; +} + +QSlider::groove:vertical +{ + border: 1px solid #303446; + width: 4px; + background: #838ba7; + margin: 0px; + border-radius: 3px; +} + +QSlider::handle:vertical +{ + background: #303446; + border: 1px solid #626880; + width: 16px; + height: 16px; + margin: 0 -8px; + border-radius: 9px; +} + +QSlider::handle:horizontal:hover, +QSlider::handle:vertical:hover +{ + background: #8caaee; +} + +QSlider::sub-page:horizontal, +QSlider::add-page:vertical +{ + background: #8caaee; + border-radius: 1px; +} + +QSlider::add-page:horizontal, +QSlider::sub-page:vertical +{ + background: #626880; + border-radius: 1px; +} + +QSlider::handle:disabled +{ + background: #303446; + border: 1px solid #737994; +} + +QSlider::groove:disabled +{ + background: #737994; + border: 1px solid #737994; +} + +QSlider::add-page:disabled, +QSlider::sub-page:disabled +{ + background: #737994; + border-radius: 1px; +} + +QToolButton#MaximizeButton { + background-color: transparent; + border-left: 1px solid QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #303446, stop: 0.3 #626880, + stop: 0.5 #737994, + stop: 0.7 #626880, stop: 1 #303446); + border-right: 1px solid QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #303446, stop: 0.3 #626880, + stop: 0.5 #737994, + stop: 0.7 #626880, stop: 1 #303446); + border-radius: 0px; + margin: 0px; + padding: 0px; +} + +QToolButton#MinimizeButton, +QToolButton#CloseButton { + background-color: transparent; + border: 1px transparent; + border-radius: 0px; + margin: 0px; + padding: 0px; +} + +QToolButton#FileCloseButton { + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #303446, stop: 0.1 #e78284, + stop: 0.5 #e78284, + stop: 0.9 #e78284, stop: 1 #303446); + border: 1px transparent; + border-radius: 0px; + margin: 0px; + padding: 0px; +} + +QToolButton#MinimizeButton:hover, QToolButton#MinimizeButton::menu-button:hover, +QToolButton#MaximizeButton:hover, QToolButton#MaximizeButton::menu-button:hover{ + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #303446, stop: 0.4 #51576d, + stop: 0.5 #51576d, + stop: 0.6 #51576d, stop: 1 #303446); +} + +QToolButton#CloseButton:hover, QToolButton#CloseButton::menu-button:hover { +background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #303446, stop: 0.2 #ea999c, + stop: 0.5 #e78284, + stop: 0.8 #ea999c, stop: 1 #303446); +} + +QToolButton#FileCloseButton:hover, QToolButton#FileCloseButton::menu-button:hover { + padding-top: 2px; +} + +QToolButton +{ + background-color: transparent; + border: 0px; + border-radius: 2px; + margin: 0px; + padding-top: 0px; + padding-left: 2px; + padding-right: 2px; + padding-bottom: 2px; +} + +QToolButton[popupMode="1"] /* only for MenuButtonPopup */ +{ + padding-right: 20px; /* make way for the popup button */ + border: 0px; + border-radius: 5px; + margin: 0px; + padding-top: 0px; + padding-left: 2px; + padding-right: 2px; + padding-bottom: 2px; +} + +QToolButton[popupMode="2"] /* only for InstantPopup */ +{ + padding-right: 10px; /* make way for the popup button */ + border: 0px; + margin: 0px; + padding-top: 0px; + padding-left: 2px; + padding-right: 2px; + padding-bottom: 2px; +} + +QToolButton:hover, +QToolButton:checked:hover, +QToolButton::menu-button:hover +QToolButton::menu-button:checked:hover +{ + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #303446, stop: 0.4 #51576d, + stop: 0.5 #51576d, + stop: 0.6 #51576d, stop: 1 #303446); + padding-top: 1px; + padding-left: 2px; + padding-right: 2px; + padding-bottom: 1px; +} + +QToolButton:checked, QToolButton:pressed, +QToolButton::menu-button:pressed { + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #303446, + stop: 0.5 #51576d, + stop: 1.0 #303446); + padding-top: 2px; + padding-left: 2px; + padding-right: 2px; + padding-bottom: 0px; +} + + +/* the subcontrols below are used only in the MenuButtonPopup mode */ +QToolButton::menu-button +{ + border: 0px; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + /* 16px width + 4px for border + no text = 20px allocated above */ + width: 16px; + outline: none; +} + +QToolButton::menu-arrow +{ + image: url(:/dark/down_arrow.svg); +} + +QToolButton::menu-arrow:open +{ + border: 1px solid #737994; +} + +QToolButton#ModeButton::menu-arrow +{ + /*image: url(:/dark/mode_down_arrow.svg);*/ + image:none; +} + +/* the subcontrol below is used only in the InstantPopup or DelayedPopup mode */ +QToolButton::menu-indicator +{ + image: url(:/dark/down_arrow.svg); + top: -20px; + left: 0px; + width:8px; + height:10px; +} + +QToolButton{ + +} + +QToolButton#ModeButton::menu-indicator +{ + image: url(:/dark/mode_down_arrow.svg); + top: 0px; + left: 0px; + width:12px; + height:12px; +} +QToolButton#ModeButton::menu-indicator:hover +{ + top: 0px; +} + +QPushButton::menu-indicator +{ + subcontrol-origin: padding; + subcontrol-position: bottom right; + left: 0px; +} + +QTableView +{ + border: 1px transparent; + gridline-color: #737994; + background-color: #232634; +} + + +QTableView, +QHeaderView +{ + border-radius: 0px; +} + +QTableView::item, +QListView::item, +QTreeView::item +{ + padding: 3px; +} + +QTableView::item:pressed, +QListView::item:pressed, +QTreeView::item:pressed +{ + background: #8caaee; + color: #c6d0f5; +} + +QTableView::item:selected:active, +QTreeView::item:selected:active, +QListView::item:selected:active +{ + background: #8caaee; + color: #c6d0f5; +} + +QTableView::item:hover, +QListView::item:hover, +QTreeView::item:hover +{ + border: 1px solid #8caaee; +} + +QHeaderView +{ + border: 1px transparent; + border-radius: 2px; + margin: 0px; + padding: 0px; +} + +QHeaderView::section +{ + background-color: #303446; + color: #c6d0f5; + padding: 4px; + border: 1px transparent; + border-radius: 0px; + text-align: center; +} + +QHeaderView::section::vertical::first, +QHeaderView::section::vertical::only-one +{ + border-top: 1px transparent; +} + +QHeaderView::section::vertical +{ + border-top: transparent; +} + +QHeaderView::section::horizontal::first, +QHeaderView::section::horizontal::only-one +{ + border-left: 1px transparent; +} + +QHeaderView::section::horizontal +{ + background-color: #626880; + border-left: transparent; +} + + +QHeaderView::section:checked +{ + color: white; + background-color: #51576d; +} + + /* style the sort indicator */ +QHeaderView::down-arrow +{ + image: url(:/dark/down_arrow.svg); +} + +QHeaderView::up-arrow +{ + image: url(:/dark/up_arrow.svg); +} + +QTableCornerButton::section +{ + background-color: #303446; + border: 1px transparent; + border-radius: 2px; +} + +QToolBox +{ + padding: 3px; + border: 1px transparent; +} + +QToolBox:selected +{ + background-color: #303446; + border-color: #8caaee; +} + +QToolBox:hover +{ + border-color: #8caaee; +} + +QStatusBar::item +{ + border: 0px transparent; +} + +QFrame[height="3"], +QFrame[width="3"] +{ + background-color: #737994; +} + +QAbstractScrollArea +{ + border-radius: 2px; + border: 0px; + background-color: #303446; +} + +QSplitter::handle:horizontal, +QMainWindow::separator +{ + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #303446, + stop: 0.4 #292c3c, + stop: 0.5 #414559, + stop: 0.6 #292c3c, + stop: 1 #303446); + color: white; + padding-left: 0px; + spacing: 0px; + width: 3px; + border: 0px solid #303446; +} + +QSplitter::handle:horizontal:hover, +QMainWindow::separator:hover +{ + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #303446, + stop: 0.1 #292c3c, + stop: 0.5 #414559, + stop: 0.9 #292c3c, + stop: 1 #303446); + color: white; + padding-left: 0px; + spacing: 0px; + width: 3px; + border: 0px solid #303446; +} + +QSplitter::handle:vertical { + background-color: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, + stop: 0.0 #303446, + stop: 0.3 #51576d, + stop: 0.5 #626880, + stop: 0.7 #51576d, + stop: 1 #303446); + height: 3px; +} + +QSplitter::handle:vertical:hover { + background-color: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, + stop: 0.0 #303446, + stop: 0.1 #51576d, + stop: 0.5 #626880, + stop: 0.8 #51576d, + stop: 1 #303446); + height: 3px; +} + + +QProgressBar:horizontal +{ + background-color: #626880; + border: 1px solid #303446; + border-radius: 3px; + height: 5px; + text-align: right; + margin-top: 5px; + margin-bottom: 5px; + margin-right: 50px; + padding: 1px; +} + +QProgressBar::chunk:horizontal +{ + background-color: #8caaee; + border: 1px transparent; + border-radius: 3px; +} + +QSpinBox, +QDoubleSpinBox +{ + padding-right: 0px; + background-color: #232634; + border-style: solid; + border: 1px solid #414559; + border-radius: 2px; + color: #c6d0f5; +} + +QSpinBox::up-button, +QDoubleSpinBox::up-button +{ + subcontrol-origin: content; + subcontrol-position: right top; + + width: 16px; + border-width: 1px; +} + +QSpinBox::up-arrow, +QDoubleSpinBox::up-arrow +{ + image: url(:/dark/up_arrow.svg); + width: 9px; + height: 6px; +} + +QSpinBox::up-arrow:hover, +QSpinBox::up-arrow:pressed, +QDoubleSpinBox::up-arrow:hover, +QDoubleSpinBox::up-arrow:pressed +{ + image: url(:/dark/up_arrow-hover.svg); + width: 9px; + height: 6px; +} + +QSpinBox::up-arrow:disabled, +QSpinBox::up-arrow:off, +QDoubleSpinBox::up-arrow:disabled, +QDoubleSpinBox::up-arrow:off +{ + image: url(:/dark/up_arrow_disabled.svg); +} + +QSpinBox::down-button, +QDoubleSpinBox::down-button +{ + subcontrol-origin: content; + subcontrol-position: right bottom; + + width: 16px; + border-width: 1px; +} + +QSpinBox::down-arrow, +QDoubleSpinBox::down-arrow +{ + image: url(:/dark/down_arrow.svg); + width: 9px; + height: 6px; +} + +QSpinBox::down-arrow:hover, +QSpinBox::down-arrow:pressed, +QDoubleSpinBox::down-arrow:hover, +QDoubleSpinBox::down-arrow:pressed +{ + image: url(:/dark/down_arrow-hover.svg); + width: 9px; + height: 6px; +} + +QSpinBox::down-arrow:disabled, +QSpinBox::down-arrow:off, +QDoubleSpinBox::down-arrow:disabled, +QDoubleSpinBox::down-arrow:off +{ + image: url(:/dark/down_arrow_disabled.svg); +} + +QTextBrowser:hover +{ + border: 1px transparent; +} + +QLineEdit#PathLine{ + color:#c6d0f5; +} +QTextEdit#PathLine{ + color:#c6d0f5; +} diff --git a/DSView/themes/latte.qss b/DSView/themes/latte.qss new file mode 100644 index 000000000..a0ee36826 --- /dev/null +++ b/DSView/themes/latte.qss @@ -0,0 +1,1695 @@ +/* + * Catppuccin Latte theme for DSView + * Generated from light.qss via a systematic + * Catppuccin color substitution - see gen_theme.py in this session's + * scratchpad for the exact mapping used. + */ +/* + * DSView light stylesheet. + * --------------------------------------------------------------------- + * The MIT License (MIT) + * + * Copyright (c) <2013-2014> + * Copyright (C) 2019 DreamSourceLab + * + * 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. + * --------------------------------------------------------------------- + */ + +QToolTip +{ + background-color: black; + color: white; + padding: 5px; +} + +QWidget +{ + color: #4c4f69; + background-color: #eff1f5; + selection-background-color:#1e66f5; + selection-color: #4c4f69; + background-clip: border; + image: none; + border: 0px transparent; + outline: 0; +} + +/* +QWidget:item:hover +{ + background-color: #1e66f5; + color: #4c4f69; +} + +QWidget:item:selected +{ + background-color: #1e66f5; +} +*/ + +QPushButton#flat{ + text-align:left; + border:none; +} + +QPushButton#flat:hover +{ + background-color: #209fb5; + color: #8839ef; +} + +QCheckBox +{ + spacing: 0px; + outline: none; + color: #4c4f69; + margin-bottom: 2px; + opacity: 200; +} + +QCheckBox:disabled +{ + color: #acb0be; +} + +QGroupBox::indicator +{ + width: 18px; + height: 18px; + margin-left: 2px; +} + +QCheckBox::indicator:unchecked, +QCheckBox::indicator:unchecked:focus +{ + image: url(:/light/checkbox_unchecked.svg); +} + +QCheckBox::indicator:unchecked:hover, +QCheckBox::indicator:unchecked:pressed, +QGroupBox::indicator:unchecked:hover, +QGroupBox::indicator:unchecked:focus, +QGroupBox::indicator:unchecked:pressed +{ + border: none; + image: url(:/light/checkbox_unchecked-hover.svg); +} + +QCheckBox::indicator:checked +{ + image: url(:/light/checkbox_checked.svg); +} + +QCheckBox::indicator:checked:hover, +QCheckBox::indicator:checked:focus, +QCheckBox::indicator:checked:pressed, +QGroupBox::indicator:checked:hover, +QGroupBox::indicator:checked:focus, +QGroupBox::indicator:checked:pressed +{ + border: none; + image: url(:/light/checkbox_checked-hover.svg); +} + +QCheckBox::indicator:indeterminate +{ + image: url(:/light/checkbox_indeterminate.svg); +} + +QCheckBox::indicator:indeterminate:focus, +QCheckBox::indicator:indeterminate:hover +QCheckBox::indicator:indeterminate:pressed +{ + image: url(:/light/checkbox_indeterminate-hover.svg); +} + +QCheckBox::indicator:indeterminate:disabled +{ + image: url(:/light/checkbox_indeterminate_disabled.svg); +} + +QCheckBox::indicator:checked:disabled, +QGroupBox::indicator:checked:disabled +{ + image: url(:/light/checkbox_checked_disabled.svg); +} + +QCheckBox::indicator:unchecked:disabled, +QGroupBox::indicator:unchecked:disabled +{ + image: url(:/light/checkbox_unchecked_disabled.svg); +} + +QRadioButton +{ + spacing: 5px; + outline: none; + color: #4c4f69; + margin-bottom: 2px; +} + +QRadioButton:disabled +{ + color: #acb0be; +} +QRadioButton::indicator +{ + width: 16px; + height: 16px; +} + +QRadioButton::indicator:unchecked, +QRadioButton::indicator:unchecked:focus +{ + image: url(:/light/radio_unchecked.svg); +} + +QRadioButton::indicator:unchecked:hover, +QRadioButton::indicator:unchecked:pressed +{ + border: none; + outline: none; + image: url(:/light/radio_unchecked-hover.svg); +} + +QRadioButton::indicator:checked +{ + border: none; + outline: none; + image: url(:/light/radio_checked.svg); +} + +QRadioButton::indicator:checked:hover, +QRadioButton::indicator:checked:focus, +QRadioButton::indicator:checked:pressed +{ + border: none; + outline: none; + image: url(:/light/radio_checked-hover.svg); +} + +QRadioButton::indicator:checked:disabled +{ + outline: none; + image: url(:/light/radio_checked_disabled.svg); +} + +QRadioButton::indicator:unchecked:disabled +{ + image: url(:/light/radio_unchecked_disabled.svg); +} + +QMenuBar +{ + background-color: #eff1f5; + color: #4c4f69; +} + +QMenuBar::item +{ + background: transparent; +} + +QMenuBar::item:selected +{ + background: transparent; + border: 1px transparent; +} + +QMenuBar::item:pressed +{ + border: 1px transparent; + background-color: #1e66f5; + color: #4c4f69; + margin-bottom: -1px; + padding-bottom: 1px; +} + +QMenu +{ + border: 1px transparent; + color: #4c4f69; + margin: 0px; +} + +QMenu::item +{ + padding: 5px 30px 5px 30px; + margin-left: 2px; + border: 1px solid transparent; /* reserve space for selection border */ +} + +QMenu::item:selected +{ + background-color: #1e66f5; + color: #4c4f69; +} + +QMenu::separator +{ + height: 2px; + background: lightblue; + margin-left: 10px; + margin-right: 5px; +} + +QMenu::indicator { + width: 18px; + height: 18px; +} + +/* non-exclusive indicator = check box style indicator + (see QActionGroup::setExclusive) */ +QMenu::indicator:non-exclusive:unchecked +{ + image: url(:/light/checkbox_unchecked_disabled.svg); +} + +QMenu::indicator:non-exclusive:unchecked:selected +{ + image: url(:/light/checkbox_unchecked_disabled.svg); +} + +QMenu::indicator:non-exclusive:checked +{ + image: url(:/light/checkbox_checked.svg); +} + +QMenu::indicator:non-exclusive:checked:selected +{ + image: url(:/light/checkbox_checked.svg); +} + +/* exclusive indicator = radio button style indicator (see QActionGroup::setExclusive) */ +QMenu::indicator:exclusive:unchecked +{ + image: url(:/light/radio_unchecked_disabled.svg); +} + +QMenu::indicator:exclusive:unchecked:selected +{ + image: url(:/light/radio_unchecked_disabled.svg); +} + +QMenu::indicator:exclusive:checked +{ + image: url(:/light/radio_checked.svg); +} + +QMenu::indicator:exclusive:checked:selected +{ + image: url(:/light/radio_checked.svg); +} + +QMenu::right-arrow +{ + margin: 5px; + image: url(:/light/right_arrow.svg); +} + + +QWidget:disabled +{ + color: #acb0be; + background-color: #eff1f5; +} + +QAbstractItemView +{ + alternate-background-color: #dce0e8; + color: #4c4f69; + border: 1px transparent; + border-radius: 2px; + padding: 1px +} + +QTabWidget:focus, +QCheckBox:focus, +QRadioButton:focus, +QSlider:focus +{ + border: none; +} + +QLineEdit +{ + background-color: #eff1f5; + padding: 2px; + border-style: solid; + border: 1px solid #4c4f69; + border-radius: 2px; + color: #4c4f69; +} + +QTextEdit +{ + background-color: #eff1f5; + padding: 2px; + border-style: solid; + border: 1px solid #4c4f69; + border-radius: 2px; + color: #4c4f69; +} + +QGroupBox +{ + border: 1px solid #4c4f69; + border-radius: 2px; + margin-top: 20px; +} + +QGroupBox:disabled +{ + border: 1px solid #acb0be; +} + +QGroupBox::title +{ + subcontrol-origin: margin; + subcontrol-position: top center; + padding-left: 10px; + padding-right: 10px; + padding-top: 10px; +} + +QScrollBar:horizontal +{ + height: 24px; + margin: 3px 12px 3px 12px; + border: 1px transparent; + border-radius: 9px; + background-color: #eff1f5; +} + +QScrollBar::handle:horizontal +{ + background-color: #6c6f85; + min-width: 20px; + border-radius: 9px; +} + +QScrollBar::add-line:horizontal +{ + margin: 0px 3px 0px 3px; + image: url(:/light/right_arrow_disabled.svg); + width: 10px; + height: 10px; + subcontrol-position: right; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:horizontal +{ + margin: 0px 3px 0px 3px; + image: url(:/light/left_arrow_disabled.svg); + width: 10px; + height: 10px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::add-line:horizontal:hover, +QScrollBar::add-line:horizontal:on +{ + image: url(:/light/right_arrow.svg); + width: 10px; + height: 10px; + subcontrol-position: right; + subcontrol-origin: margin; +} + + +QScrollBar::sub-line:horizontal:hover, +QScrollBar::sub-line:horizontal:on +{ + image: url(:/light/left_arrow.svg); + width: 10px; + height: 10px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:horizontal, +QScrollBar::down-arrow:horizontal +{ + background: none; +} + + +QScrollBar::add-page:horizontal, +QScrollBar::sub-page:horizontal +{ + background: none; +} + +QScrollBar:vertical +{ + background-color: #eff1f5; + width: 24px; + margin: 12px 3px 12px 3px; + border: 1px transparent; + border-radius: 9px; +} + +QScrollBar::handle:vertical +{ + background-color: #6c6f85; + min-height: 20px; + border-radius: 9px; +} + +QScrollBar::sub-line:vertical +{ + margin: 3px 0px 3px 0px; + image: url(:/light/up_arrow_disabled.svg); + height: 10px; + width: 10px; + subcontrol-position: top; + subcontrol-origin: margin; +} + +QScrollBar::add-line:vertical +{ + margin: 3px 0px 3px 0px; + image: url(:/light/down_arrow_disabled.svg); + height: 10px; + width: 10px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:vertical:hover, +QScrollBar::sub-line:vertical:on +{ + + image: url(:/light/up_arrow.svg); + height: 10px; + width: 10px; + subcontrol-position: top; + subcontrol-origin: margin; +} + + +QScrollBar::add-line:vertical:hover, +QScrollBar::add-line:vertical:on +{ + image: url(:/light/down_arrow.svg); + height: 10px; + width: 10px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:vertical, +QScrollBar::down-arrow:vertical +{ + background: none; +} + + +QScrollBar::add-page:vertical, +QScrollBar::sub-page:vertical +{ + background: none; +} + +QTextEdit +{ + background-color: #eff1f5; + color: #4c4f69; + border: 1px solid #4c4f69; + margin: 0; +} + +QPlainTextEdit +{ + background-color: #eff1f5; + color: #4c4f69; + border-radius: 2px; + border: 1px solid #4c4f69; +} + +QHeaderView::section +{ + background-color: #4c4f69; + color: #4c4f69; + padding-left: 4px; + border: 1px solid #4c4f69; +} + +QSizeGrip +{ + image: url(:/light/sizegrip.svg); + width: 12px; + height: 12px; +} + +QMenu::separator +{ + height: 1px; + background-color: #4c4f69; + color: white; + padding-left: 4px; + margin-left: 10px; + margin-right: 5px; +} + +QFrame +{ + border-radius: 2px; + border: 1px transparent; +} + +QFrame[frameShape="0"] +{ + border-radius: 2px; + border: 1px transparent; +} + +QStackedWidget +{ + border: 1px transparent; +} + +QToolBar +{ + border: 1px transparent; + background: transparent; + padding: 0px; +} + +QToolBar::handle:horizontal +{ + image: url(:/light/hmovetoolbar.svg); + width = 16px; + height = 64px; +} + +QToolBar::handle:vertical +{ + image: url(:/light/vmovetoolbar.svg); + width = 54px; + height = 10px; +} + +QToolBar::separator:horizontal +{ + image: url(:/light/hsepartoolbar.svg); + width = 7px; + height = 63px; +} + +QToolBar::separator:vertical +{ + image: url(:/light/vsepartoolbars.svg); + width = 63px; + height = 7px; +} + +QPushButton +{ + color: #4c4f69; + background-color: #eff1f5; + border-width: 1px; + border-color: #4c4f69; + border-style: solid; + padding: 5px; + border-radius: 5px; + outline: none; +} + +QPushButton:disabled +{ + background-color: #dce0e8; + border-width: 1px; + border-color: #acb0be; + border-style: solid; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 10px; + padding-right: 10px; + border-radius: 5px; + color: #acb0be; +} + +QPushButton:focus +{ + color: black; +} + +QPushButton:pressed +{ + background-color: #eff1f5; + padding-top: -15px; + padding-bottom: -17px; +} + + +QPushButton:checked{ + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #eff1f5, + stop: 0.5 #9ca0b0, + stop: 1 #eff1f5); +} + +QPushButton:hover +{ + background-color: #eff1f5; + border: 1px solid #1e66f5; + color: #4c4f69; + padding-top: 6px; + padding-bottom: 4px; +} + +QPushButton:checked:hover +{ + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #eff1f5, + stop: 0.5 #9ca0b0, + stop: 1 #eff1f5); + border: 1px solid #1e66f5; + color: #4c4f69; + padding-top: 6px; + padding-bottom: 4px; +} + +QComboBox:hover, +QAbstractSpinBox:hover, +QLineEdit:hover, +QTextEdit:hover, +QPlainTextEdit:hover, +QAbstractView:hover, +QTreeView:hover +{ + border: 1px solid #1e66f5; + color: #4c4f69; +} + +QComboBox:hover:pressed, +QPushButton:hover:pressed, +QAbstractSpinBox:hover:pressed, +QLineEdit:hover:pressed, +QTextEdit:hover:pressed, +QPlainTextEdit:hover:pressed, +QAbstractView:hover:pressed, +QTreeView:hover:pressed +{ + background-color: #eff1f5; +} + +QComboBox:disabled, +QAbstractSpinBox:disabled, +QLineEdit:disabled, +QTextEdit:disabled, +QPlainTextEdit:disabled, +QAbstractView:disabled, +QTreeView:disabled +{ + border: 1px solid #acb0be; +} + +QComboBox:on +{ + padding-top: 3px; + padding-left: 4px; + selection-background-color: #5c5f77; +} + +QComboBox +{ + selection-background-color: #1e66f5; + background-color: #eff1f5; + border-style: solid; + border: 1px solid #4c4f69; + border-radius: 2px; + padding: 2px; + min-width: 30px; +} + +QComboBox QAbstractItemView +{ + background-color: #eff1f5; + border-radius: 2px; + border: 1px solid #4c4f69; + selection-background-color: #1e66f5; +} + +QComboBox::drop-down +{ + subcontrol-origin: padding; + subcontrol-position: top right; + width: 10px; + + border-left-width: 0px; + border-left-color: darkgray; + border-left-style: solid; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +QComboBox::down-arrow +{ + image: url(:/light/down_arrow_disabled.svg); +} + +QComboBox::down-arrow:on, +QComboBox::down-arrow:hover, +QComboBox::down-arrow:focus +{ + image: url(:/light/down_arrow.svg); +} + +QAbstractSpinBox +{ + padding: 2px; + border: 1px solid #4c4f69; + background-color: #e6e9ef; + color: #4c4f69; + border-radius: 2px; + min-width: 60px; +} + +QAbstractSpinBox:up-button +{ + background-color: transparent; + subcontrol-origin: border; + subcontrol-position: center right; +} + +QAbstractSpinBox:down-button +{ + background-color: transparent; + subcontrol-origin: border; + subcontrol-position: center left; +} + +QAbstractSpinBox::up-arrow, +QAbstractSpinBox::up-arrow:disabled, +QAbstractSpinBox::up-arrow:off +{ + image: url(:/light/up_arrow_disabled.svg); + width: 10px; + height: 10px; +} + +QAbstractSpinBox::up-arrow:hover +{ + image: url(:/light/up_arrow.svg); +} + +QAbstractSpinBox::down-arrow, +QAbstractSpinBox::down-arrow:disabled, +QAbstractSpinBox::down-arrow:off +{ + image: url(:/light/down_arrow_disabled.svg); + width: 10px; + height: 10px; +} + +QAbstractSpinBox::down-arrow:hover +{ + image: url(:/light/down_arrow.svg); +} + +QLabel +{ + border: 0px solid black; + margin-left: 2px; + margin-right: 2px; +} + +/* BORDERS */ +QTabWidget::pane +{ + padding: 0px; + padding-right: 3px; + margin: 0px; +} + +QTabWidget::pane:top +{ + border: 1px transparent; + top: -1px; +} + +QTabWidget::pane:bottom +{ + border: 1px transparent; + bottom: -1px; +} + +QTabWidget::pane:left +{ + border: 1px transparent; + right: -1px; +} + +QTabWidget::pane:right +{ + border: 1px transparent; + left: -1px; +} + +QTabBar +{ + qproperty-drawBase: 0; + margin: 0px; + padding: 0px; + border-radius: 3px; +} + +QTabBar:focus +{ + border: 0px transparent; +} + +QTabBar::close-button +{ + image: url(:/light/close.svg); + background: transparent; +} + +QTabBar::close-button:hover +{ + image: url(:/light/close-hover.svg); + width: 12px; + height: 12px; + background: transparent; +} + +QTabBar::close-button:pressed +{ + image: url(:/light/close-pressed.svg); + width: 12px; + height: 12px; + background: transparent; +} + +QTabBar::tab +{ + color: 2A2A2A; + background-color: #209fb5; + padding: 5px; +} + +QTabBar::tab:!selected +{ + color: 2A2A2A; + background-color: #eff1f5; +} + +QTabBar::tab:disabled +{ + color: #acb0be; + background-color: #eff1f5; +} + +/* TOP TABS */ +QTabBar::tab:top +{ + border: 1px solid #acb0be; + border-bottom: 1px transparent; + border-top-left-radius: 2px; + border-top-right-radius: 2px; +} + +QTabBar::tab:top:!selected +{ + border: 1px solid #acb0be; + border-bottom: 1px transparent; + border-top-left-radius: 2px; + border-top-right-radius: 2px; +} + +QTabBar::tab:top:hover +{ + border: 1px solid #1e66f5; + border-bottom: 1px transparent; +} + + +/* BOTTOM TABS */ +QTabBar::tab:bottom +{ + border: 1px solid #acb0be; + border-top: 1px transparent; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} + +QTabBar::tab:bottom:!selected +{ + border: 1px solid #acb0be; + border-top: 1px transparent; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} + +QTabBar::tab:bottom:hover +{ + border: 1px solid #1e66f5; + border-top: 1px transparent; +} + + +/* LEFT TABS */ +QTabBar::tab:left +{ + border: 1px solid #acb0be; + border-left: 1px transparent; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; +} + +QTabBar::tab:left:!selected +{ + border: 1px solid #acb0be; + border-left: 1px transparent; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; +} + +QTabBar::tab:left:hover +{ + border: 1px solid #1e66f5; + border-left: 1px transparent; +} + + +/* RIGHT TABS */ +QTabBar::tab:right +{ + border: 1px solid #acb0be; + border-right: 1px transparent; + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} + +QTabBar::tab:right:!selected +{ + border: 1px solid #acb0be; + border-right: 1px transparent; + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} + +QTabBar::tab:right:hover +{ + border: 1px solid #1e66f5; + border-right: 1px transparent; +} + + +QTabBar QToolButton::right-arrow:enabled +{ + image: url(:/light/right_arrow.svg); +} + +QTabBar QToolButton::left-arrow:enabled +{ + image: url(:/light/left_arrow.svg); +} + +QTabBar QToolButton::right-arrow:disabled +{ + image: url(:/light/right_arrow_disabled.svg); +} + +QTabBar QToolButton::left-arrow:disabled +{ + image: url(:/light/left_arrow_disabled.svg); +} + +QDockWidget +{ + background: #eff1f5; + border: 1px transparent; + titlebar-close-icon: url(:/light/transparent.svg); + titlebar-normal-icon: url(:/light/transparent.svg); +} + +QDockWidget::close-button, +QDockWidget::float-button +{ + border: 1px solid transparent; + border-radius: 2px; + background: transparent; +} + +QDockWidget::float-button +{ + image: url(:/dark/undock.svg); +} + +QDockWidget::float-button:hover +{ + image: url(:/dark/undock-hover.svg) ; +} + +QDockWidget::close-button +{ + image: url(:/dark/close.svg) ; +} + +QDockWidget::close-button:hover +{ + image: url(:/dark/close-hover.svg) ; +} + +QDockWidget::close-button:pressed +{ + image: url(:/dark/close-pressed.svg) ; +} + +QTreeView, +QListView +{ + border: 1px solid #4c4f69; + background-color: #eff1f5; +} + + +QTreeView::branch:has-siblings:!adjoins-item +{ + image: url(:/light/stylesheet-vline.svg) 0; +} + +QTreeView::branch:has-siblings:adjoins-item +{ + image: url(:/light/stylesheet-branch-more.svg) 0; +} + +QTreeView::branch:!has-children:!has-siblings:adjoins-item +{ + image: url(:/light/stylesheet-branch-end.svg) 0; +} + +QTreeView::branch:has-children:!has-siblings:closed, +QTreeView::branch:closed:has-children:has-siblings +{ + image: url(:/light/stylesheet-branch-end-closed.svg) 0; + image: url(:/light/branch_closed.svg); +} + +QTreeView::branch:open:has-children:!has-siblings, +QTreeView::branch:open:has-children:has-siblings +{ + image: url(:/light/stylesheet-branch-end-open.svg) 0; + image: url(:/light/branch_open.svg); +} + +QTableView::item, +QListView::item, +QTreeView::item +{ + padding: 3px; +} + +QTableView::item:!selected:hover, +QListView::item:!selected:hover, +QTreeView::item:!selected:hover +{ + background-color: rgba(61, 173, 232, 0.1); + outline: 0; + color: #4c4f69; + padding: 3px; +} + +QSlider::groove:horizontal +{ + border: 1px solid #eff1f5; + height: 4px; + background: #6c6f85; + margin: 0px; + border-radius: 1px; +} + +QSlider::handle:horizontal +{ + background: #eff1f5; + border: 1px solid #1e66f5; + width: 16px; + margin: -7px 0; + border-radius: 8px; +} + +QSlider::groove:vertical +{ + border: 1px solid #eff1f5; + width: 4px; + background: #6c6f85; + margin: 0px; + border-radius: 3px; +} + +QSlider::handle:vertical +{ + background: #eff1f5; + border: 1px solid #1e66f5; + width: 16px; + height: 16px; + margin: 0 -8px; + border-radius: 9px; +} + +QSlider::handle:horizontal:hover, +QSlider::handle:vertical:hover +{ + background: #1e66f5; +} + +QSlider::sub-page:horizontal, +QSlider::add-page:vertical +{ + background: #1e66f5; + border-radius: 1px; +} + +QSlider::add-page:horizontal, +QSlider::sub-page:vertical +{ + background: #6c6f85; + border-radius: 1px; +} + +QSlider::handle:disabled +{ + background: #eff1f5; + border: 1px solid #acb0be; +} + +QSlider::groove:disabled +{ + background: #acb0be; + border: 1px solid #acb0be; +} + +QSlider::add-page:disabled, +QSlider::sub-page:disabled +{ + background: #acb0be; + border-radius: 1px; +} + +QToolButton#MaximizeButton { + background-color: transparent; + border-left: 1px solid QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #eff1f5, stop: 0.3 #5c5f77, + stop: 0.5 #8c8fa1, + stop: 0.7 #5c5f77, stop: 1 #eff1f5); + border-right: 1px solid QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #eff1f5, stop: 0.3 #5c5f77, + stop: 0.5 #8c8fa1, + stop: 0.7 #5c5f77, stop: 1 #eff1f5); + border-radius: 0px; + margin: 0px; + padding: 0px; +} + +QToolButton#MinimizeButton, +QToolButton#CloseButton { + background-color: transparent; + border: 1px transparent; + border-radius: 0px; + margin: 0px; + padding: 0px; +} + +QToolButton#FileCloseButton { + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #eff1f5, stop: 0.1 #d20f39, + stop: 0.5 #d20f39, + stop: 0.9 #d20f39, stop: 1 #eff1f5); + border: 1px transparent; + border-radius: 0px; + margin: 0px; + padding: 0px; +} + +QToolButton#MinimizeButton:hover, QToolButton#MinimizeButton::menu-button:hover, +QToolButton#MaximizeButton:hover, QToolButton#MaximizeButton::menu-button:hover{ + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #eff1f5, stop: 0.4 #5c5f77, + stop: 0.5 #5c5f77, + stop: 0.6 #5c5f77, stop: 1 #eff1f5); +} + +QToolButton#CloseButton:hover, QToolButton#CloseButton::menu-button:hover { +background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #eff1f5, stop: 0.1 #d20f39, + stop: 0.5 #d20f39, + stop: 0.9 #d20f39, stop: 1 #eff1f5); +} + +QToolButton#FileCloseButton:hover, QToolButton#FileCloseButton::menu-button:hover { + padding-top: 2px; +} + +QToolButton +{ + background-color: transparent; + border: 0px; + border-radius: 0px; + margin: 0px; + padding-top: 0px; + padding-left: 2px; + padding-right: 2px; + padding-bottom: 2px; +} + +QToolButton[popupMode="1"] /* only for MenuButtonPopup */ +{ + padding-right: 20px; /* make way for the popup button */ + border: 0px; + border-radius: 0px; + margin: 0px; + padding-top: 0px; + padding-left: 2px; + padding-right: 2px; + padding-bottom: 2px; +} + +QToolButton[popupMode="2"] /* only for InstantPopup */ +{ + padding-right: 10px; /* make way for the popup button */ + border: 0px; + margin: 0px; + padding-top: 0px; + padding-left: 2px; + padding-right: 2px; + padding-bottom: 2px; +} + +QToolButton:hover, +QToolButton::menu-button:hover +{ + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #eff1f5, stop: 0.9 #eff1f5, + stop: 0.95 #1e66f5, stop: 1 #1e66f5); + padding-top: 1px; + padding-left: 2px; + padding-right: 2px; + padding-bottom: 1px; +} + +QToolButton:checked, QToolButton:pressed, +QToolButton::menu-button:pressed { + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #eff1f5, stop: 0.9 #eff1f5, + stop: 0.95 #1e66f5, stop: 1 #1e66f5); + padding-top: 2px; + padding-left: 2px; + padding-right: 2px; + padding-bottom: 0px; +} + + +/* the subcontrols below are used only in the MenuButtonPopup mode */ +QToolButton::menu-button +{ + border: 0px; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + /* 16px width + 4px for border + no text = 20px allocated above */ + width: 16px; + outline: none; +} + +QToolButton::menu-arrow +{ + image: url(:/light/down_arrow.svg); +} +QToolButton::menu-arrow:open +{ + border: 1px solid #4c4f69; +} + +QToolButton#ModeButton::menu-arrow +{ + /*image: url(:/light/mode_down_arrow.svg);*/ + image:none; +} + +/* the subcontrol below is used only in the InstantPopup or DelayedPopup mode */ +QToolButton::menu-indicator +{ + image: url(:/light/down_arrow.svg); + top: -20px; + left: 0px; + width:8px; + height:10px; +} + +QToolButton{ + +} + +QToolButton#ModeButton::menu-indicator +{ + image: url(:/light/mode_down_arrow.svg); + top: 0px; + left: 0px; + width:12px; + height:12px; +} +QToolButton#ModeButton::menu-indicator:hover +{ + top: 0px; +} + +QPushButton::menu-indicator +{ + subcontrol-origin: padding; + subcontrol-position: bottom right; + left: 0px; +} + +QTableView +{ + border: 1px transparent; + gridline-color: #4c4f69; + background-color: #eff1f5; +} + + +QTableView, +QHeaderView +{ + border-radius: 0px; +} + +QTableView::item:pressed +{ + background: #1e66f5; + color: #4c4f69; +} + +QTableView::item:selected:active +{ + background: #1e66f5; + color: #4c4f69; +} + +QTableView::item:selected:hover +{ + background-color: #04a5e5; + color: #4c4f69; +} + +QListView::item:pressed, +QTreeView::item:pressed +{ + background: #1e66f5; + color: #4c4f69; +} + +QTreeView::item:selected:active, +QListView::item:selected:active +{ + background: #1e66f5; + color: #4c4f69; +} + +QTableView::item:hover, +QListView::item:hover, +QTreeView::item:hover +{ + border: 1px solid #1e66f5; +} + + +QHeaderView +{ + border: 1px transparent; + border-radius: 2px; + margin: 0px; + padding: 0px; +} + +QHeaderView::section +{ + background-color: #eff1f5; + color: #4c4f69; + padding: 4px; + border: 1px transparent; + border-radius: 0px; + text-align: center; +} + +QHeaderView::section::vertical::first, +QHeaderView::section::vertical::only-one +{ + border-top: 1px transparent; +} + +QHeaderView::section::vertical +{ + border-top: transparent; +} + +QHeaderView::section::horizontal::first, +QHeaderView::section::horizontal::only-one +{ + border-left: 1px transparent; +} + +QHeaderView::section::horizontal +{ + background-color: #ccd0da; + border-left: transparent; +} + + +QHeaderView::section:checked + + { + color: black; + background-color: #99d1db; + } + + /* style the sort indicator */ +QHeaderView::down-arrow +{ + image: url(:/light/down_arrow.svg); +} + +QHeaderView::up-arrow +{ + image: url(:/light/up_arrow.svg); +} + +QTableCornerButton::section +{ + background-color: #eff1f5; + border: 1px transparent; + border-radius: 0px; +} + +QToolBox +{ + padding: 3px; + border: 1px transparent; +} + +QToolBox:selected +{ + background-color: #eff1f5; + border-color: #1e66f5; +} + +QToolBox:hover +{ + border-color: #1e66f5; +} + +QStatusBar::item +{ + border: 0px transparent; +} + +QSplitter::handle:horizontal +QMainWindow::separator +{ + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #eff1f5, + stop: 0.4 #ccd0da, + stop: 0.5 #6c6f85, + stop: 0.6 #ccd0da, + stop: 1 #eff1f5); + color: white; + padding-left: 0px; + spacing: 0px; + width: 2px; + border: 0px solid #4c4f69; +} + +QSplitter::handle:horizontal:hover, +QMainWindow::separator:hover +{ + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 #eff1f5, + stop: 0.1 #ccd0da, + stop: 0.5 #6c6f85, + stop: 0.9 #ccd0da, + stop: 1 #eff1f5); + color: white; + padding-left: 0px; + spacing: 0px; + width: 2px; + border: 0px solid #4c4f69; +} + +QSplitter::handle:vertical { + background-color: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, + stop: 0.0 #eff1f5, + stop: 0.3 #ccd0da, + stop: 0.5 #6c6f85, + stop: 0.7 #ccd0da, + stop: 1 #eff1f5); + height: 3px; +} + +QSplitter::handle:vertical:hover { + background-color: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, + stop: 0.0 #eff1f5, + stop: 0.1 #ccd0da, + stop: 0.5 #6c6f85, + stop: 0.8 #ccd0da, + stop: 1 #eff1f5); + height: 3px; +} + + +QProgressBar:horizontal +{ + background-color: #bcc0cc; + border: 1px solid #eff1f5; + border-radius: 3px; + height: 5px; + text-align: right; + margin-top: 5px; + margin-bottom: 5px; + margin-right: 50px; + padding: 1px; +} + +QProgressBar::chunk:horizontal +{ + background-color: #1e66f5; + border: 1px transparent; + border-radius: 3px; +} + +QAbstractSpinBox +{ + background-color: #eff1f5; +} + +QSpinBox, +QDoubleSpinBox +{ + padding-right: 0px; +} + +QSpinBox::up-button, +QDoubleSpinBox::up-button +{ + subcontrol-origin: content; + subcontrol-position: right top; + + width: 16px; + border-width: 1px; +} + +QSpinBox::up-arrow, +QDoubleSpinBox::up-arrow +{ + image: url(:/light/up_arrow.svg); + width: 9px; + height: 6px; +} + +QSpinBox::up-arrow:hover, +QSpinBox::up-arrow:pressed, +QDoubleSpinBox::up-arrow:hover, +QDoubleSpinBox::up-arrow:pressed +{ + image: url(:/light/up_arrow-hover.svg); + width: 9px; + height: 6px; +} + +QSpinBox::up-arrow:disabled, +QSpinBox::up-arrow:off, +QDoubleSpinBox::up-arrow:disabled, +QDoubleSpinBox::up-arrow:off +{ + image: url(:/light/up_arrow_disabled.svg); +} + +QSpinBox::down-button, +QDoubleSpinBox::down-button +{ + subcontrol-origin: content; + subcontrol-position: right bottom; + + width: 16px; + border-width: 1px; +} + +QSpinBox::down-arrow, +QDoubleSpinBox::down-arrow +{ + image: url(:/light/down_arrow.svg); + width: 9px; + height: 6px; +} + +QSpinBox::down-arrow:hover, +QSpinBox::down-arrow:pressed, +QDoubleSpinBox::down-arrow:hover, +QDoubleSpinBox::down-arrow:pressed +{ + image: url(:/light/down_arrow-hover.svg); + width: 9px; + height: 6px; +} + +QSpinBox::down-arrow:disabled, +QSpinBox::down-arrow:off, +QDoubleSpinBox::down-arrow:disabled, +QDoubleSpinBox::down-arrow:off +{ + image: url(:/light/down_arrow_disabled.svg); +} + +QTextBrowser:hover +{ + border: 1px transparent; +} + +QLineEdit#PathLine{ + color:#4c4f69; +} + +QTextEdit#PathLine{ + color:#4c4f69; +} diff --git a/README.md b/README.md index f3fb71985..8c852c46d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![DreamSourceLab Logo](DSView/icons/dsl_logo.svg) +# DSView -# DSView DSView is a GUI program for supporting various instruments from [DreamSourceLab](http://www.dreamsourcelab.com), including logic analyzers, oscilloscopes, etc. DSView is based on the [sigrok project](https://sigrok.org). The sigrok project aims at creating a portable, cross-platform, Free/Libre/Open-Source signal analysis software suite that supports various device types (such as logic analyzers, oscilloscopes, multimeters, and more). @@ -10,6 +10,10 @@ The sigrok project aims at creating a portable, cross-platform, Free/Libre/Open- The DSView software is in a usable state and has official tarball releases. However, it is still a work in progress. Some basic functionality is available and working, but other things are always on the TODO list. +# Download + +Pre-built binaries are available on the [releases page](https://github.com/Schildkroet/DSView/releases). + # Useful links - [dreamsourcelab.com](https://www.dreamsourcelab.com) diff --git a/common/minizip/crypt.h b/common/minizip/crypt.h index 1e9e8200b..d22a7d817 100644 --- a/common/minizip/crypt.h +++ b/common/minizip/crypt.h @@ -37,6 +37,7 @@ static int decrypt_byte(unsigned long* pkeys, const z_crc_t* pcrc_32_tab) unsigned temp; /* POTENTIAL BUG: temp*(temp^1) may overflow in an * unpredictable manner on 16-bit systems; not a problem * with any known compiler so far, though */ + (void)pcrc_32_tab; temp = ((unsigned)(*(pkeys+2)) & 0xffff) | 2; return (int)(((temp * (temp ^ 1)) >> 8) & 0xff); diff --git a/common/minizip/ioapi.c b/common/minizip/ioapi.c index 3c5c1dbf6..86d4b5f34 100644 --- a/common/minizip/ioapi.c +++ b/common/minizip/ioapi.c @@ -98,6 +98,7 @@ static int ZCALLBACK ferror_file_func OF((voidpf opaque, voidpf stream)); static voidpf ZCALLBACK fopen_file_func (voidpf opaque, const char* filename, int mode) { + (void)opaque; FILE* file = NULL; const char* mode_fopen = NULL; if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ) @@ -116,6 +117,7 @@ static voidpf ZCALLBACK fopen_file_func (voidpf opaque, const char* filename, in static voidpf ZCALLBACK fopen64_file_func (voidpf opaque, const void* filename, int mode) { + (void)opaque; #ifdef _WIN32 FILE* file = NULL; const wchar_t* mode_fopen = NULL; @@ -164,6 +166,7 @@ static voidpf ZCALLBACK fopen64_file_func (voidpf opaque, const void* filename, static uLong ZCALLBACK fread_file_func (voidpf opaque, voidpf stream, void* buf, uLong size) { + (void)opaque; uLong ret; ret = (uLong)fread(buf, 1, (size_t)size, (FILE *)stream); return ret; @@ -171,6 +174,7 @@ static uLong ZCALLBACK fread_file_func (voidpf opaque, voidpf stream, void* buf, static uLong ZCALLBACK fwrite_file_func (voidpf opaque, voidpf stream, const void* buf, uLong size) { + (void)opaque; uLong ret; ret = (uLong)fwrite(buf, 1, (size_t)size, (FILE *)stream); return ret; @@ -178,6 +182,7 @@ static uLong ZCALLBACK fwrite_file_func (voidpf opaque, voidpf stream, const voi static long ZCALLBACK ftell_file_func (voidpf opaque, voidpf stream) { + (void)opaque; long ret; ret = ftell((FILE *)stream); return ret; @@ -186,6 +191,7 @@ static long ZCALLBACK ftell_file_func (voidpf opaque, voidpf stream) static ZPOS64_T ZCALLBACK ftell64_file_func (voidpf opaque, voidpf stream) { + (void)opaque; ZPOS64_T ret; ret = FTELLO_FUNC((FILE *)stream); return ret; @@ -193,6 +199,7 @@ static ZPOS64_T ZCALLBACK ftell64_file_func (voidpf opaque, voidpf stream) static long ZCALLBACK fseek_file_func (voidpf opaque, voidpf stream, uLong offset, int origin) { + (void)opaque; int fseek_origin=0; long ret; switch (origin) @@ -216,6 +223,7 @@ static long ZCALLBACK fseek_file_func (voidpf opaque, voidpf stream, uLong offs static long ZCALLBACK fseek64_file_func (voidpf opaque, voidpf stream, ZPOS64_T offset, int origin) { + (void)opaque; int fseek_origin=0; long ret; switch (origin) @@ -242,6 +250,7 @@ static long ZCALLBACK fseek64_file_func (voidpf opaque, voidpf stream, ZPOS64_T static int ZCALLBACK fclose_file_func (voidpf opaque, voidpf stream) { + (void)opaque; int ret; ret = fclose((FILE *)stream); return ret; @@ -249,6 +258,7 @@ static int ZCALLBACK fclose_file_func (voidpf opaque, voidpf stream) static int ZCALLBACK ferror_file_func (voidpf opaque, voidpf stream) { + (void)opaque; int ret; ret = ferror((FILE *)stream); return ret; diff --git a/common/minizip/zip.c b/common/minizip/zip.c index 44e88a9cb..360aecf8e 100644 --- a/common/minizip/zip.c +++ b/common/minizip/zip.c @@ -518,15 +518,16 @@ local ZPOS64_T zip64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_f if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize) break; - for (i=(int)uReadSize-3; (i--)>0;) + for (i=(int)uReadSize-3; (i--)>0;) { if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) && ((*(buf+i+2))==0x05) && ((*(buf+i+3))==0x06)) { uPosFound = uReadPos+i; break; } + } - if (uPosFound!=0) + if (uPosFound!=0) break; } TRYFREE(buf); diff --git a/installer/windows/dsl_usb_instruments.inf b/installer/windows/dsl_usb_instruments.inf new file mode 100644 index 000000000..9ae71a0e8 --- /dev/null +++ b/installer/windows/dsl_usb_instruments.inf @@ -0,0 +1,91 @@ +; WinUSB driver binding for DreamSourceLab USB instruments (DSLogic/DSCope +; family), for use with `pnputil /add-driver dsl_usb_instruments.inf /install`. +; +; Trimmed from DreamSourceLab's own official installer +; (DSView_v1.3.2_x64_setup.exe, Drivers\dsl_usb_instruments.inf): the +; [Manufacturer]/model list and the WinUSB-binding sections below are copied +; verbatim from that proven-working INF. The WDF co-installer sections +; (CoInstallers/CopyFiles/SourceDisks*) were dropped - those exist only to +; support Windows versions older than 10, where KMDF/WinUSB were not yet +; in-box; on Windows 10/11 they are unnecessary, so no co-installer DLLs need +; to be bundled or redistributed here. +; +; If DreamSourceLab ships a new product with a new USB PID, both this file +; and libsigrok4DSL/hardware/DSL/dsl.h's device tables need updating - there +; is no automatic sync between the two. + +[Version] +Signature = "$Windows NT$" +Class = USB +ClassGUID={36FC9E60-C465-11CF-8056-444553540000} +Provider = %Provider% +DriverVer=11/02/2006,6.0.6000.16388 + + +; ========== Manufacturer/Models sections =========== + +[Manufacturer] +%Provider% = DreamSourceLabMfg,NTamd64 + +[DreamSourceLabMfg.NTamd64] +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0001 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0002 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0003 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0004 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0005 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0006 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0007 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0008 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0009 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_000A +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_000B +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_000C +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_000D +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_000E +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_000F +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0010 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0011 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0012 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0013 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0014 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0015 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0016 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0017 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0018 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_0019 +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_001A +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_001B +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_001C +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_001D +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_001E +%USB\DreamSourceLab.DeviceDesc% = DSL_Install, USB\VID_2A0E&PID_001F + +; =================== Installation =================== + +[DSL_Install] +Include=winusb.inf +Needs=WINUSB.NT + +[DSL_Install.Services] +Include=winusb.inf +AddService=WinUSB,0x00000002,WinUSB_ServiceInstall + +[WinUSB_ServiceInstall] +DisplayName = %WinUSB_SvcDesc% +ServiceType = 1 +StartType = 3 +ErrorControl = 1 +ServiceBinary = %12%\WinUSB.sys + +[DSL_Install.Wdf] +KmdfService=WINUSB, WinUsb_Install + +[WinUSB_Install] +KmdfLibraryVersion=1.7 + +; =================== Strings =================== + +[Strings] +Provider="DreamSourceLab" +USB\DreamSourceLab.DeviceDesc="DreamSourceLab USB-based Instrument" +WinUSB_SvcDesc="DreamSourceLab USB-based Instrument" diff --git a/installer/windows/dsview.iss b/installer/windows/dsview.iss new file mode 100644 index 000000000..47ac354e9 --- /dev/null +++ b/installer/windows/dsview.iss @@ -0,0 +1,62 @@ +; Inno Setup script for the Windows DSView installer. +; +; Unlike the plain portable zip (the other Windows CI artifact), this +; installer also stages the WinUSB driver binding for DreamSourceLab hardware +; (dsl_usb_instruments.inf, in this same directory) via pnputil, so a fresh +; Windows install detects the hardware without any extra manual driver setup. +; +; Expects to be compiled with the working directory at the repo root, e.g.: +; iscc installer\windows\dsview.iss /DMyAppVersion=1.5.0 +; MyAppVersion defaults below if not passed on the command line. +; +; Source paths below are relative to this script's own directory (Inno +; Setup's default SourceDir), so they reach up to the repo-root-relative +; dsview-dist\ folder that the CI workflow's earlier steps already assemble +; (see .github/workflows/build.yml, the "Install" and "Bundle runtime DLLs" +; steps of the windows job). + +#ifndef MyAppVersion + #define MyAppVersion "1.5.0" +#endif +#define MyAppName "DSView" +#define MyAppPublisher "DreamSourceLab" +#define DistDir "..\..\dsview-dist" + +[Setup] +AppId={{7E5B6A4E-5B7B-4C7A-9C7B-6A6E3C1B6A4E} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppPublisher={#MyAppPublisher} +DefaultDirName={autopf}\{#MyAppName} +DefaultGroupName={#MyAppName} +DisableProgramGroupPage=yes +; The whole install (including the pnputil driver-staging step below) needs +; to run elevated; this avoids a second, separate UAC prompt mid-install. +PrivilegesRequired=admin +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +Compression=lzma2 +SolidCompression=yes +OutputDir=Output +OutputBaseFilename=DSView-windows-x86_64-setup +UninstallDisplayIcon={app}\DSView.exe + +[Files] +Source: "{#DistDir}\*"; DestDir: "{app}"; Flags: recursesubdirs ignoreversion +Source: "dsl_usb_instruments.inf"; DestDir: "{app}\drivers"; Flags: ignoreversion +; Lets MainFrame::show_driver_hint_once() (mainframe.cpp) tell apart an +; installed copy (driver already staged below) from the portable zip (where +; it still points the user at Zadig, since pnputil can't run unelevated). +Source: "installed_via_setup.marker"; DestDir: "{app}"; Flags: ignoreversion + +[Icons] +Name: "{group}\{#MyAppName}"; Filename: "{app}\DSView.exe" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\DSView.exe"; Tasks: desktopicon + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Run] +Filename: "{sys}\pnputil.exe"; Parameters: "/add-driver ""{app}\drivers\dsl_usb_instruments.inf"" /install"; \ + StatusMsg: "Installing DreamSourceLab USB driver..."; Flags: runhidden waituntilterminated +Filename: "{app}\DSView.exe"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: nowait postinstall skipifsilent diff --git a/installer/windows/installed_via_setup.marker b/installer/windows/installed_via_setup.marker new file mode 100644 index 000000000..5608d4511 --- /dev/null +++ b/installer/windows/installed_via_setup.marker @@ -0,0 +1,4 @@ +This file's presence tells DSView (MainFrame::show_driver_hint_once() in +mainframe.cpp) that it was installed via the Inno Setup installer, which +already staged the WinUSB driver (dsl_usb_instruments.inf) via pnputil during +setup - so the first-start driver hint dialog should stay silent. diff --git a/lang/cn/dlg.json b/lang/cn/dlg.json index 22db6d62a..ebf66f594 100644 --- a/lang/cn/dlg.json +++ b/lang/cn/dlg.json @@ -699,6 +699,10 @@ "id": "IDS_DLG_DISPLAY_ANTIALIAS", "text": "抗锯齿" }, + { + "id": "IDS_DLG_DONT_ASK_SAVE_ON_EXIT", + "text": "不再询问是否保存采集数据" + }, { "id": "IDS_DLG_SERIAL_HEX", "text": "十六进制 :" @@ -806,5 +810,37 @@ { "id": "IDS_FFT_MODE_LINEARRSM", "text": "线性 RMS" + }, + { + "id": "IDS_DLG_DECODER_DYNAMIC_FONT_WIDTH", + "text": "解码器自适应字体宽度" + }, + { + "id": "IDS_DLG_DEFAULT_FONT", + "text": "默认" + }, + { + "id": "IDS_DLG_MAX_FONT_WIDTH", + "text": "最大字体宽度: " + }, + { + "id": "IDS_DLG_MIN_FONT_WIDTH", + "text": "最小字体宽度: " + }, + { + "id": "IDS_DLG_RULER_UNITS", + "text": "标尺 / 光标单位" + }, + { + "id": "IDS_DLG_VERTICAL_SCROLL_ACTION", + "text": "垂直滚动操作" + }, + { + "id": "IDS_DLG_VERTICAL_SCROLL_ACTION_SMOOTH_ZOOM", + "text": "缩放" + }, + { + "id": "IDS_DLG_VERTICAL_SCROLL_ACTION_VERTICAL_SCROLL", + "text": "滚动" } ] diff --git a/lang/cn/toolbar.json b/lang/cn/toolbar.json index 65b94f007..30d767b3d 100644 --- a/lang/cn/toolbar.json +++ b/lang/cn/toolbar.json @@ -106,7 +106,15 @@ { "id": "IDS_TOOLBAR_DISPLAY_THEMES_LIGHT", "text": "清新(&L)" - }, + }, + { + "id": "IDS_TOOLBAR_DISPLAY_THEMES_LATTE", + "text": "拿铁(&T)" + }, + { + "id": "IDS_TOOLBAR_DISPLAY_THEMES_FRAPPE", + "text": "冰沙(&P)" + }, { "id": "IDS_TOOLBAR_DISPLAY_OPTIONS", "text": "选项(&O)" @@ -163,6 +171,10 @@ "id": "IDS_TOOLBAR_HELP_LANG_CN", "text": "中文(&C)" }, + { + "id": "IDS_TOOLBAR_HELP_LANG_DE", + "text": "德语(&D)" + }, { "id": "IDS_TOOLBAR_HELP_ABOUT", "text": "关于(&A)" diff --git a/lang/de/dlg.json b/lang/de/dlg.json new file mode 100644 index 000000000..c02d34b72 --- /dev/null +++ b/lang/de/dlg.json @@ -0,0 +1,842 @@ +[ + { + "id": "IDS_DLG_LOG_OPTIONS", + "text": "Protokolloptionen" + }, + { + "id": "IDS_DLG_LOG_LEVEL", + "text": "Protokollstufe" + }, + { + "id": "IDS_DLG_SAVE_FILE", + "text": "In Datei speichern" + }, + { + "id": "IDS_DLG_OPEN_FILE", + "text": "Datei öffnen" + }, + { + "id": "IDS_DLG_OPEN_SEESION", + "text": "Sitzung öffnen" + }, + { + "id": "IDS_DLG_SAVE_SEESION", + "text": "Sitzung speichern" + }, + { + "id": "IDS_DLG_DOCUMENT", + "text": "Dokument" + }, + { + "id": "IDS_DLG_TRIGGER_DOCK_TITLE", + "text": "Triggereinstellung" + }, + { + "id": "IDS_DLG_PROTOCOL_DOCK_TITLE", + "text": "Dekodierer" + }, + { + "id": "IDS_DLG_MEASURE_DOCK_TITLE", + "text": "Messung" + }, + { + "id": "IDS_DLG_SEARCH_DOCK_TITLE", + "text": "Suchen" + }, + { + "id": "IDS_DLG_SAVE_AS", + "text": "Speichern unter" + }, + { + "id": "IDS_DLG_CHECK_USB_SPEED_ERROR", + "text": "Das aktuelle Gerät an einen USB 2.0 Anschluss anzuschließen beeinträchtigt die Leistung erheblich.\nBitte an einen USB 3.0 Anschluss umstecken." + }, + { + "id": "IDS_DLG_CHECK_SESSION_FILE_VERSION_ERROR", + "text": "Diese Datei liegt in einem alten Format vor.\nSie wird langsam geladen.\nSie können sie nach dem Laden erneut speichern." + }, + { + "id": "IDS_DLG_ABOUT", + "text": "Über" + }, + { + "id": "IDS_DLG_DISPLAY_OPTIONS", + "text": "Anzeigeoptionen" + }, + { + "id": "IDS_DLG_QUICK_SCROLL", + "text": "Wellenform-Scrollen mit Mausbewegung" + }, + { + "id": "IDS_DLG_SAVE", + "text": "Speichern" + }, + { + "id": "IDS_DLG_RESET", + "text": "Zurücksetzen" + }, + { + "id": "IDS_DLG_EXIT", + "text": "Beenden" + }, + { + "id": "IDS_DLG_MANUAL_CALIBRATION", + "text": "Manuelle Kalibrierung" + }, + { + "id": "IDS_DLG_CHANNEL", + "text": "Kanal" + }, + { + "id": "IDS_DLG_SAVE_CALIBRATION_RESULTS", + "text": "Kalibrierungsergebnisse werden gespeichert... Bitte warten." + }, + { + "id": "IDS_DLG_CANCEL", + "text": "Abbrechen" + }, + { + "id": "IDS_DLG_RELOAD_CALIBRATION_RESULTS", + "text": "Vorherige Kalibrierungsergebnisse werden neu geladen... Bitte warten." + }, + { + "id": "IDS_DLG_DECODER_OPTIONS", + "text": "Dekodierer-Optionen" + }, + { + "id": "IDS_DLG_CURSOR", + "text": "Cursor " + }, + { + "id": "IDS_DLG_CURSOR_FOR_DECODE_START", + "text": "Dekodierungs-Startposition" + }, + { + "id": "IDS_DLG_CURSOR_FOR_DECODE_END", + "text": "Dekodierungs-Endposition" + }, + { + "id": "IDS_DLG_DEVICE_OPTIONS", + "text": "Geräteoptionen" + }, + { + "id": "IDS_DLG_MODE", + "text": "Optionen" + }, + { + "id": "IDS_DLG_ENABLE_ALL", + "text": "Alle aktivieren" + }, + { + "id": "IDS_DLG_DISABLE_ALL", + "text": "Alle deaktivieren" + }, + { + "id": "IDS_DLG_ENABLE", + "text": "Aktivieren: " + }, + { + "id": "IDS_DLG_AUTO_CALIBRATION", + "text": "Automatische Kalibrierung" + }, + { + "id": "IDS_DLG_CALIBRATION", + "text": "Kalibrierung" + }, + { + "id": "IDS_DLG_MEASUREMENTS", + "text": "Messungen" + }, + { + "id": "IDS_DLG_FFT_ENABLE", + "text": "FFT aktivieren: " + }, + { + "id": "IDS_DLG_FFT_LENGTH", + "text": "FFT-Länge: " + }, + { + "id": "IDS_DLG_SAMPLE_INTERVAL", + "text": "Abtastintervall: " + }, + { + "id": "IDS_DLG_FFT_SOURCE", + "text": "FFT-Quelle: " + }, + { + "id": "IDS_DLG_FFT_WINDOW", + "text": "FFT-Fenster: " + }, + { + "id": "IDS_DLG_DC_IGNORED", + "text": "DC ignoriert: " + }, + { + "id": "IDS_DLG_Y-AXIS_MODE", + "text": "Y-Achsen-Modus: " + }, + { + "id": "IDS_DLG_DBV_RANGE", + "text": "DBV-Bereich: " + }, + { + "id": "IDS_DLG_FFT_OPTIONS", + "text": "FFT-Optionen" + }, + { + "id": "IDS_DLG_INTERVAL_S", + "text": "Intervall (s): " + }, + { + "id": "IDS_DLG_REPETITIVE_INTERVAL", + "text": "Wiederholungsintervall" + }, + { + "id": "IDS_DLG_X_AXIS", + "text": "X-Achse" + }, + { + "id": "IDS_DLG_Y_AXIS", + "text": "Y-Achse" + }, + { + "id": "IDS_DLG_LISSAJOUS_OPTIONS", + "text": "Lissajous-Optionen" + }, + { + "id": "IDS_DLG_ADD", + "text": "Hinzufügen" + }, + { + "id": "IDS_DLG_SUBSTRACT", + "text": "Subtrahieren" + }, + { + "id": "IDS_DLG_MULTIPLY", + "text": "Multiplizieren" + }, + { + "id": "IDS_DLG_DIVIDE", + "text": "Dividieren" + }, + { + "id": "IDS_DLG_MATH_TYPE", + "text": "Mathematiktyp" + }, + { + "id": "IDS_DLG_1ST_SOURCE", + "text": "1. Quelle" + }, + { + "id": "IDS_DLG_2ST_SOURCE", + "text": "2. Quelle" + }, + { + "id": "IDS_DLG_MATH_OPTIONS", + "text": "Mathematik-Optionen" + }, + { + "id": "IDS_DLG_EXPORT_FORMAT", + "text": "Exportformat: " + }, + { + "id": "IDS_DLG_PROTOCOL_EXPORT", + "text": "Protokoll-Export" + }, + { + "id": "IDS_DLG_EXPORT_DATA", + "text": "Daten exportieren: " + }, + { + "id": "IDS_DLG_EXPORT_PROTOCOL_LIST_RESULT", + "text": "Dekodierungsergebnisse werden exportiert... Bitte warten." + }, + { + "id": "IDS_DLG_FIT_TO_WINDOW", + "text": "An Fenstergröße anpassen" + }, + { + "id": "IDS_DLG_FIXED", + "text": "Fest" + }, + { + "id": "IDS_DLG_MAP_ZOOM", + "text": "Sprungzoom: " + }, + { + "id": "IDS_DLG_DECODED_PROTOCOLS", + "text": "Dekodierte Protokolle: " + }, + { + "id": "IDS_DLG_PROTOCOL_LIST_VIEWER", + "text": "Dekodierungsergebnisse" + }, + { + "id": "IDS_DLG_REGION", + "text": "Bereich" + }, + { + "id": "IDS_DLG_SEARCH_LABEL", + "text": "X: Beliebig\n0: Niedriger Pegel\n1: Hoher Pegel\nR: Steigende Flanke\nF: Fallende Flanke\nC: Steigende/Fallende Flanke" + }, + { + "id": "IDS_DLG_SEARCH_OPTIONS", + "text": "Suchoptionen" + }, + { + "id": "IDS_DLG_SAVING", + "text": "Speichern..." + }, + { + "id": "IDS_DLG_ORIGINAL_DATA", + "text": "Originaldaten" + }, + { + "id": "IDS_DLG_COMPRESSED_DATA", + "text": "Komprimierte Daten" + }, + { + "id": "IDS_DLG_EXPORT", + "text": "Exportieren" + }, + { + "id": "IDS_DLG_EXPORTING", + "text": "Exportieren..." + }, + { + "id": "IDS_DLG_DONT_CONNECT_PROBES", + "text": "Keine Sonden anschließen!" + }, + { + "id": "IDS_DLG_LOAD_CURRENT_SETTING", + "text": "Aktuelle Einstellungen werden geladen... Bitte warten." + }, + { + "id": "IDS_DLG_WAITING", + "text": "Warten" + }, + { + "id": "IDS_DLG_FINISHED", + "text": "Fertig!" + }, + { + "id": "IDS_DLG_US", + "text": "µs" + }, + { + "id": "IDS_DLG_MS", + "text": "ms" + }, + { + "id": "IDS_DLG_S", + "text": "s" + }, + { + "id": "IDS_DLG_TRIGGER_POSITION", + "text": "Triggerposition: " + }, + { + "id": "IDS_DLG_HOLD_OFF_TIME", + "text": "Haltezeit: " + }, + { + "id": "IDS_DLG_NOISE_SENSITIVITY", + "text": "Trigger-Empfindlichkeit: " + }, + { + "id": "IDS_DLG_TRIGGER_SOURCES", + "text": "Triggerquellen: " + }, + { + "id": "IDS_DLG_TRIGGER_TYPES", + "text": "Triggertypen: " + }, + { + "id": "IDS_DLG_RISING_EDGE", + "text": "Steigende Flanke" + }, + { + "id": "IDS_DLG_FALLING_EDGE", + "text": "Fallende Flanke" + }, + { + "id": "IDS_DLG_AUTO", + "text": "Automatisch" + }, + { + "id": "IDS_DLG_CHANNEL_0", + "text": "Kanal 0" + }, + { + "id": "IDS_DLG_CHANNEL_1", + "text": "Kanal 1" + }, + { + "id": "IDS_DLG_CHANNEL_0_AND_1", + "text": "Kanal 0 && 1" + }, + { + "id": "IDS_DLG_CHANNEL_0_OR_1", + "text": "Kanal 0 | 1" + }, + { + "id": "IDS_DLG_KEY_DECODER_SEARCH", + "text": "Dekodierer suchen..." + }, + { + "id": "IDS_DLG_TIME_SAMPLES", + "text": "Zeit / Abtastwerte" + }, + { + "id": "IDS_DLG_MOUSE_MEASUREMENT", + "text": "Mausmessung" + }, + { + "id": "IDS_DLG_ENABLE_FLOATING_MEASUREMENT", + "text": "Schwebende Messung aktivieren" + }, + { + "id": "IDS_DLG_CURSOR_DISTANCE", + "text": "Cursorabstand" + }, + { + "id": "IDS_DLG_EDGES", + "text": "Flanken" + }, + { + "id": "IDS_DLG_CURSORS", + "text": "Cursor" + }, + { + "id": "IDS_DLG_RIS_OR_FAL_EDGE", + "text": "Steigende/Fallende Flanken" + }, + { + "id": "IDS_DLG_W", + "text": "Breite: " + }, + { + "id": "IDS_DLG_P", + "text": "Periode: " + }, + { + "id": "IDS_DLG_F", + "text": "Freq.: " + }, + { + "id": "IDS_DLG_D", + "text": "Tast: " + }, + { + "id": "IDS_DLG_SEARCH", + "text": "Suchen" + }, + { + "id": "IDS_DLG_MATCHING_ITEMS", + "text": "Treffer:" + }, + { + "id": "IDS_DLG_OUT_OF_MEMORY", + "text": "Kein Speicher mehr" + }, + { + "id": "IDS_DLG_SEARCHING", + "text": "Suchen..." + }, + { + "id": "IDS_DLG_SEARCH_PREVIOUS", + "text": "Vorherigen suchen..." + }, + { + "id": "IDS_DLG_SEARCH_NEXT", + "text": "Nächsten suchen..." + }, + { + "id": "IDS_DLG_SIMPLE_TRIGGER", + "text": "Einfacher Trigger" + }, + { + "id": "IDS_DLG_ADVANCED_TRIGGER", + "text": "Erweiterter Trigger" + }, + { + "id": "IDS_DLG_TOTAL_TRIGGER_STAGES", + "text": "Gesamte Triggerstufen: " + }, + { + "id": "IDS_DLG_START_FLAG", + "text": "Startflag: " + }, + { + "id": "IDS_DLG_STOP_FLAG", + "text": "Stoppflag: " + }, + { + "id": "IDS_DLG_CLOCK_FLAG", + "text": "Taktflag: " + }, + { + "id": "IDS_DLG_DATA_CHANNEL", + "text": "Datenkanal: " + }, + { + "id": "IDS_DLG_DATA_VALUE", + "text": "Datenwert: " + }, + { + "id": "IDS_DLG_SERIAL_TRIGGER", + "text": "Serieller Trigger" + }, + { + "id": "IDS_DLG_STAGE_TRIGGER", + "text": "Stufentrigger" + }, + { + "id": "IDS_DLG_SERIAL_NOTE_LABEL", + "text": "X: Beliebig\n0: Niedriger Pegel\n1: Hoher Pegel\nR: Steigende Flanke\nF: Fallende Flanke\nC: Steigende/Fallende Flanke" + }, + { + "id": "IDS_DLG_DATA_BITS", + "text": "Datenbits" + }, + { + "id": "IDS_DLG_INV", + "text": "Inv" + }, + { + "id": "IDS_DLG_COUNTER", + "text": "Zähler" + }, + { + "id": "IDS_DLG_CONTIGUOUS", + "text": "Zusammenhängend" + }, + { + "id": "IDS_DLG_STAGE", + "text": "Stufe" + }, + { + "id": "IDS_DLG_OR", + "text": "Oder" + }, + { + "id": "IDS_DLG_AND", + "text": "Und" + }, + { + "id": "IDS_DLG_HZ", + "text": "Hz" + }, + { + "id": "IDS_DLG_UNSHOWN", + "text": "Ausgeblendet" + }, + { + "id": "ZOOM_IN_FOR_DETAILS", + "text": "Für Details hineinzoomen" + }, + { + "id": "IDS_DLG_DECODETRACE_ERROR1", + "text": "Fehler: " + }, + { + "id": "IDS_DLG_DECODETRACE_ERROR2", + "text": "Fehler: ..." + }, + { + "id": "IDS_DLG_ADD_GROUP", + "text": "Gruppe hinzufügen" + }, + { + "id": "IDS_DLG_DEL_GROUP", + "text": "Gruppe entfernen" + }, + { + "id": "IDS_DLG_SET_CHANNEL_COLOUR", + "text": "Kanalfarbe festlegen" + }, + { + "id": "IDS_DLG_LISSAJOUS_FIGURE", + "text": "Lissajous-Figur" + }, + { + "id": "IDS_DLG_DATA_SOURCE_ERROR", + "text": "Datenquellenfehler." + }, + { + "id": "IDS_DLG_ADD_X_CURSOR", + "text": "X-Cursor hinzufügen" + }, + { + "id": "IDS_DLG_ADD_Y_CURSOR", + "text": "Y-Cursor hinzufügen" + }, + { + "id": "IDS_DLG_AUTO_ROLL", + "text": "Auto(Rollen)" + }, + { + "id": "IDS_DLG_WAITING_TRIG", + "text": "Warte auf Trigger" + }, + { + "id": "IDS_DLG_TRIG_D", + "text": "Getriggert" + }, + { + "id": "IDS_DLG_TRIGGERED", + "text": "Getriggert! " + }, + { + "id": "IDS_DLG_CAPTURED", + "text": "% Erfasst" + }, + { + "id": "IDS_DLG_WAITING_FOR_TRIGGER", + "text": "Warte auf Trigger! " + }, + { + "id": "IDS_DLG_RISING", + "text": "Steigend: " + }, + { + "id": "IDS_DLG_FALLING", + "text": "Fallend: " + }, + { + "id": "IDS_DLG_Edges_1", + "text": "Flanken: " + }, + { + "id": "IDS_DLG_WIDTH", + "text": "Breite: " + }, + { + "id": "IDS_DLG_PERIOD", + "text": "Periode: " + }, + { + "id": "IDS_DLG_FREQUENCY", + "text": "Frequenz: " + }, + { + "id": "IDS_DLG_DUTY_CYCLE", + "text": "Tastverhältnis: " + }, + { + "id": "IDS_DLG_SAMPLES_MEAS", + "text": "Abtastwerte: " + }, + { + "id": "IDS_DLG_SAMPLES", + "text": "Abtastwerte" + }, + { + "id": "IDS_DLG_TIME", + "text": "Zeit" + }, + { + "id": "IDS_DLG_RULER_UNITS", + "text": "Lineal / Cursor-Einheiten" + }, + { + "id": "IDS_DLG_MEASURE", + "text": "Messen" + }, + { + "id": "IDS_DLG_TRIGGER_TIME", + "text": "Triggerzeit: " + }, + { + "id": "IDS_DLG_SAMPLES_CAPTURED", + "text": "Abtastwerte erfasst!" + }, + { + "id": "IDS_DLG_FILE_THRESHOLD", + "text": "Schwellenwert: " + }, + { + "id": "IDS_DLG_VIEW_CAPTURE", + "text": "Aufnehmen" + }, + { + "id": "IDS_DLG_CHAN_NUM_ERR2", + "text": "Benötigt die Daten von zwei Kanälen." + }, + { + "id": "IDS_DLG_PATH_NAME", + "text": "Pfad" + }, + { + "id": "IDS_DLG_DECODER_IF_TRANS", + "text": "Parameternamen übersetzen" + }, + { + "id": "IDS_DLG_LOG_PATH", + "text": "Dateipfad" + }, + { + "id": "IDS_DLG_OPEN", + "text": "Öffnen" + }, + { + "id": "IDS_DLG_CLEARE", + "text": "Löschen" + }, + { + "id": "IDS_DLG_APPEND_MODE", + "text": "Anhängemodus" + }, + { + "id": "IDS_DLG_TRIG_DISPLAY_MIDDLE", + "text": "Triggerpos. in der Mitte anzeigen" + }, + { + "id": "IDS_DLG_DISPLAY_PROFILE_IN_BAR", + "text": "Profil in der Titelleiste anzeigen" + }, + { + "id": "IDS_DLG_DISPLAY_ANTIALIAS", + "text": "Kantenglättung" + }, + { + "id": "IDS_DLG_DONT_ASK_SAVE_ON_EXIT", + "text": "Nicht nach Speichern der aufgezeichneten Daten fragen" + }, + { + "id": "IDS_DLG_SERIAL_HEX", + "text": "Hex:" + }, + { + "id": "IDS_DLG_SERIAL_INPUT_AS_HEX", + "text": "Eingabe im Hex-Format" + }, + { + "id": "IDS_DLG_USE_ABORT_DATA_REPEAT", + "text": "Neueste Daten beim Stopp im Wiederholmodus aktualisieren" + }, + { + "id": "IDS_DLG_FONT_SIZE", + "text": "Schriftgröße" + }, + { + "id": "IDS_DLG_GROUP_LOGIC", + "text": "Logikanalysator" + }, + { + "id": "IDS_DLG_GROUP_DSO", + "text": "Oszilloskop" + }, + { + "id": "IDS_DLG_GROUP_UI", + "text": "Benutzeroberfläche" + }, + { + "id": "IDS_DLG_ABORT", + "text": "Abbrechen" + }, + { + "id": "IDS_DLG_AUTO_SCROLL_LATEAST_DATA", + "text": "Automatisch zu den neuesten Daten scrollen" + }, + { + "id": "IDS_DLG_DATA_OUT_OFF_RANGE", + "text": "Daten außerhalb des Bereichs" + }, + { + "id": "IDS_DLG_START_CURSOR", + "text": "Start" + }, + { + "id": "IDS_DLG_END_CURSOR", + "text": "Ende" + }, + { + "id": "IDS_DSO_CTR_EN", + "text": "EIN" + }, + { + "id": "IDS_DSO_CTR_DIS", + "text": "AUS" + }, + { + "id": "IDS_DSO_CTR_GND", + "text": "GND" + }, + { + "id": "IDS_DSO_CTR_DC", + "text": "DC" + }, + { + "id": "IDS_DSO_CTR_AC", + "text": "AC" + }, + { + "id": "IDS_DSO_CTR_AUTO", + "text": "AUTO" + }, + { + "id": "IDS_CALIB_VGAIN", + "text": "VGAIN" + }, + { + "id": "IDS_CALIB_VOFF", + "text": "VOFF" + }, + { + "id": "IDS_CALIB_VCOMB", + "text": "VCOMB" + }, + { + "id": "IDS_FFT_WINDOW_RECTANGLE", + "text": "Rechteck" + }, + { + "id": "IDS_FFT_WINDOW_HANN", + "text": "Hann" + }, + { + "id": "IDS_FFT_WINDOW_HAMMING", + "text": "Hamming" + }, + { + "id": "IDS_FFT_WINDOW_BLACKMAN", + "text": "Blackman" + }, + { + "id": "IDS_FFT_WINDOW_FLATTOP", + "text": "Flache Spitze" + }, + { + "id": "IDS_FFT_MODE_LINEARRSM", + "text": "Linear RMS" + }, + { + "id": "IDS_DLG_DECODER_DYNAMIC_FONT_WIDTH", + "text": "Adaptive Dekodierer-Schriftbreite" + }, + { + "id": "IDS_DLG_DEFAULT_FONT", + "text": "Standard" + }, + { + "id": "IDS_DLG_MAX_FONT_WIDTH", + "text": "Max. Schriftbreite:" + }, + { + "id": "IDS_DLG_MIN_FONT_WIDTH", + "text": "Min. Schriftbreite:" + }, + { + "id": "IDS_DLG_VERTICAL_SCROLL_ACTION", + "text": "Vertikale Scroll-Aktion" + }, + { + "id": "IDS_DLG_VERTICAL_SCROLL_ACTION_SMOOTH_ZOOM", + "text": "Zoomen" + }, + { + "id": "IDS_DLG_VERTICAL_SCROLL_ACTION_VERTICAL_SCROLL", + "text": "Scrollen" + } +] diff --git a/lang/de/msg.json b/lang/de/msg.json new file mode 100644 index 000000000..8e01d10ef --- /dev/null +++ b/lang/de/msg.json @@ -0,0 +1,390 @@ +[ + { + "id": "IDS_MSG_OK", + "text": "Ok" + }, + { + "id": "IDS_MSG_SKIP", + "text": "Überspringen" + }, + { + "id": "IDS_MSG_CANCEL", + "text": "Abbrechen" + }, + { + "id": "IDS_MSG_ATTENTION", + "text": "Achtung" + }, + { + "id": "IDS_MSG_ERROR", + "text": "Fehler" + }, + { + "id": "IDS_MSG_INFORMATION", + "text": "Information" + }, + { + "id": "IDS_MSG_CONTINUE", + "text": "Weiter" + }, + { + "id": "IDS_MSG_AUTO_CALIB_START", + "text": "Die automatische Kalibrierung wird gestartet. Keine Sonden anschließen. \nDies kann einen Moment dauern!" + }, + { + "id": "IDS_MSG_AUTO_CALIB", + "text": "Automatische Kalibrierung" + }, + { + "id": "IDS_MSG_ADJUST_SAVE", + "text": "Bitte Nullpunkt-Offset anpassen und das Ergebnis speichern" + }, + { + "id": "IDS_MSG_SAVE_CAPDATE", + "text": "Aufgezeichnete Daten speichern?" + }, + { + "id": "IDS_MSG_NOT_FOND_DEFAULT_PROFILE", + "text": "Keine Standardsitzung für dieses Gerät gefunden!" + }, + { + "id": "IDS_MSG_OPEN_FILE_ERROR", + "text": "Fehler beim Öffnen der Datei!" + }, + { + "id": "IDS_MSG_PROFILE_NOT_COMPATIBLE", + "text": "Sitzung ist nicht kompatibel mit dem aktuellen Gerät oder Modus!" + }, + { + "id": "IDS_MSG_RESTORE_WINDOW_ERROR", + "text": "Fehler beim Wiederherstellen des Fensterstatus!" + }, + { + "id": "IDS_MSG_DECODE_INVAILD_CURSOR", + "text": "Ungültiger Cursorindex für den Abtastbereich!" + }, + { + "id": "IDS_MSG_SEL_FILENAME", + "text": "Bitte einen Dateinamen auswählen." + }, + { + "id": "IDS_MSG_DECODER_REPEAT", + "text": "Doppelte ID oder Name in den Dekodierern gefunden:" + }, + { + "id": "IDS_MSG_DECODER_LIST_EMPTY", + "text": "Dekodierliste ist leer!" + }, + { + "id": "IDS_MSG_NO_SEL_DECODER", + "text": "Bitte einen Dekodierer auswählen!" + }, + { + "id": "IDS_MSG_FIND_BASE_DECODER_ERROR", + "text": "Kein entsprechender Basis-Dekodierer gefunden!" + }, + { + "id": "IDS_MSG_NO_DECODER_DEL", + "text": "Kein Dekodierer zum Entfernen vorhanden!" + }, + { + "id": "IDS_MSG_DECODER_CONFIRM_DEL_ALL", + "text": "Alle Dekodierer wirklich entfernen?" + }, + { + "id": "IDS_MSG_DECODER_CONFIRM_DEL", + "text": "Diesen Dekodierer wirklich entfernen?" + }, + { + "id": "IDS_MSG_CLOSE_DEVICE", + "text": "Dieses Gerät wirklich entfernen?" + }, + { + "id": "IDS_MSG_SET_DEF_CAL_SETTING", + "text": "Alle Kalibrierungseinstellungen werden als Standard gespeichert!" + }, + { + "id": "IDS_MSG_ALL_CHANNEL_DISABLE", + "text": "Alle Kanäle deaktiviert! Bitte mindestens einen Kanal aktivieren." + }, + { + "id": "IDS_MSG_MAX_CHANNEL_COUNT_WARNING", + "text": "Aktueller Modus unterstützt maximal {0} Kanäle!" + }, + { + "id": "IDS_MSG_TRI_SET_ISSUE", + "text": "Problem mit der Triggereinstellung" + }, + { + "id": "IDS_MSG_CHANGE_HOR_TRI_POS_FAIL", + "text": "Änderung der horizontalen Triggerposition fehlgeschlagen!" + }, + { + "id": "IDS_MSG_CHANGE_TRI_HOLDOFF_TIME_FAIL", + "text": "Änderung der Trigger-Haltezeit fehlgeschlagen!" + }, + { + "id": "IDS_MSG_CHANGE_SENSITIVITY_FAIL", + "text": "Änderung der Trigger-Empfindlichkeit fehlgeschlagen!" + }, + { + "id": "IDS_MSG_CHANGE_SOURCE_FAIL", + "text": "Änderung der Triggerquelle fehlgeschlagen!" + }, + { + "id": "IDS_MSG_CHANGE_CHANNEL_FAIL", + "text": "Änderung des Triggerkanals fehlgeschlagen!" + }, + { + "id": "IDS_MSG_CHANGE_TYPE_FAIL", + "text": "Änderung des Triggertyps fehlgeschlagen!" + }, + { + "id": "IDS_MSG_PLEASE_INSERT_CURSOR", + "text": "Bitte zuerst einen Cursor einfügen, um die Cursormessung zu verwenden." + }, + { + "id": "IDS_MSG_SEARCH", + "text": "Suchen" + }, + { + "id": "IDS_MSG_NO_SAMPLE_DATA", + "text": "Keine Abtastdaten vorhanden!" + }, + { + "id": "IDS_MSG_SEARCH_AT_START", + "text": "Suchcursor an Anfangsposition!" + }, + { + "id": "IDS_MSG_PATTERN_NOT_FOUND", + "text": "Muster nicht gefunden!" + }, + { + "id": "IDS_MSG_SEARCH_AT_END", + "text": "Suchcursor an Endposition!" + }, + { + "id": "IDS_MSG_TRIGGER", + "text": "Trigger" + }, + { + "id": "IDS_MSG_STREAM_NO_AD_TRIGGER", + "text": "Stream-Modus unterstützt keinen erweiterten Trigger!" + }, + { + "id": "IDS_MSG_AD_TRIGGER_NEED_HARDWARE", + "text": "Erweiterter Trigger benötigt DSLogic-Hardware-Unterstützung!" + }, + { + "id": "IDS_MSG_SET_TRI_MULTI_CHANNEL", + "text": "Trigger auf mehreren Kanälen gesetzt!\nAufnahme wird nur ausgelöst, wenn alle gesetzten Kanäle zur gleichen Abtastzeit erfüllt sind" + }, + { + "id": "IDS_MSG_NOT_SHOW_AGAIN", + "text": "Nicht mehr anzeigen" + }, + { + "id": "IDS_MSG_CLEAR_TRIG", + "text": "Trigger löschen" + }, + { + "id": "IDS_MSG_IGNORE", + "text": "Ignorieren" + }, + { + "id": "IDS_MSG_OPEN", + "text": "Öffnen" + }, + { + "id": "IDS_MSG_FAIL_TO_LOAD", + "text": "Laden fehlgeschlagen" + }, + { + "id": "IDS_MSG_HARDWARE_ERROR", + "text": "Hardwarefehler" + }, + { + "id": "IDS_MSG_HARDWARE_ERROR_DET", + "text": "Bitte Gerät erneut einstecken, um die Hardwarekonfiguration zu aktualisieren!" + }, + { + "id": "IDS_MSG_MALLOC_ERROR", + "text": "Speicherreservierungsfehler" + }, + { + "id": "IDS_MSG_MALLOC_ERROR_DET", + "text": "Nicht genug Speicher für diese Abtastung!\nBitte Abtasttiefe reduzieren!" + }, + { + "id": "IDS_MSG_PACKET_ERROR", + "text": "Paketfehler" + }, + { + "id": "IDS_MSG_PACKET_ERROR_DET", + "text": "Das empfangene Paketformat ist falsch!" + }, + { + "id": "IDS_MSG_DATA_OVERFLOW", + "text": "Datenüberlauf" + }, + { + "id": "IDS_MSG_DATA_OVERFLOW_DET", + "text": "USB-Bandbreite kann die aktuelle Abtastrate nicht unterstützen!\nBitte Abtastrate reduzieren!" + }, + { + "id": "IDS_MSG_UNDEFINED_ERROR", + "text": "Unbekannter Fehler" + }, + { + "id": "IDS_MSG_UNDEFINED_ERROR_DET", + "text": "Unerwarteter Fehler!" + }, + { + "id": "IDS_MSG_STORESESS_SAVESTART_ERROR1", + "text": "DSView unterstützt derzeit keine\nDateispeicherung für mehrere Datentypen." + }, + { + "id": "IDS_MSG_STORESESS_SAVESTART_ERROR2", + "text": "Keine Daten zum Speichern." + }, + { + "id": "IDS_MSG_STORESESS_SAVESTART_ERROR3", + "text": "Kein Dateiname." + }, + { + "id": "IDS_MSG_STORESESS_SAVESTART_ERROR4", + "text": "Erstellen der temporären Dateidaten fehlgeschlagen." + }, + { + "id": "IDS_MSG_STORESESS_SAVESTART_ERROR5", + "text": "Erstellen der Dekodierer-Dateidaten fehlgeschlagen." + }, + { + "id": "IDS_MSG_STORESESS_SAVESTART_ERROR6", + "text": "Erstellen der Sitzungs-Dateidaten fehlgeschlagen." + }, + { + "id": "IDS_MSG_STORESESS_SAVESTART_ERROR7", + "text": "Erstellen der ZIP-Datei fehlgeschlagen." + }, + { + "id": "IDS_MSG_STORESESS_SAVEPROC_ERROR1", + "text": "ZIP-Datei konnte nicht erstellt werden, Speicherreservierungsfehler." + }, + { + "id": "IDS_MSG_STORESESS_SAVEPROC_ERROR2", + "text": "ZIP-Datei konnte nicht erstellt werden, bitte Schreibberechtigung für diesen Pfad prüfen." + }, + { + "id": "IDS_MSG_STORESESS_EXPORTSTART_ERROR1", + "text": "DSView unterstützt derzeit keinen\nDateiexport für mehrere Datentypen." + }, + { + "id": "IDS_MSG_STORESESS_EXPORTSTART_ERROR2", + "text": "Keine Daten zum Speichern." + }, + { + "id": "IDS_MSG_STORESESS_EXPORTSTART_ERROR3", + "text": "Kein Dateiname gesetzt." + }, + { + "id": "IDS_MSG_STORESESS_EXPORTSTART_ERROR4", + "text": "Ungültiges Exportformat." + }, + { + "id": "IDS_MSG_STORESESS_EXPORTPROC_ERROR1", + "text": "Datentyp wird nicht unterstützt." + }, + { + "id": "IDS_MSG_STORESESS_EXPORTPROC_ERROR2", + "text": "Pufferspeicher fehlgeschlagen." + }, + { + "id": "IDS_MSG_SAVE_FILE", + "text": "Datei speichern" + }, + { + "id": "IDS_MSG_EXPORT_DATA", + "text": "Daten exportieren" + }, + { + "id": "IDS_MSG_DECODERSTACK_DECODE_WORK_ERROR", + "text": "Ein oder mehrere erforderliche Kanäle wurden nicht angegeben!" + }, + { + "id": "IDS_MSG_DECODERSTACK_DECODE_DATA_ERROR", + "text": "Mindestens einer der ausgewählten Kanäle ist nicht aktiviert." + }, + { + "id": "IDS_MSG_DECODERSTACK_DECODE_STACK_ERROR", + "text": "Dekodierer-Instanz konnte nicht erstellt werden" + }, + { + "id": "IDS_MSG_MESSAGE", + "text": "Nachricht" + }, + { + "id": "IDS_MSG_BOX_CONFIRM", + "text": "Bestätigen" + }, + { + "id": "IDS_MSG_NO_DATA", + "text": "Keine Daten vorhanden!" + }, + { + "id": "IDS_MSG_TO_SWITCH_DEVICE", + "text": "Zum neuen Gerät wechseln?" + }, + { + "id": "IDS_MSG_DISABLED_CHANNEL_TRIG", + "text": "Deaktivierte Kanäle können nicht für den Trigger verwendet werden!" + }, + { + "id": "IDS_MSG_TO_RECONNECT_FOR_FIRMWARE", + "text": "Die Firmware-Version ist falsch, bitte Gerät erneut einstecken!" + }, + { + "id": "IDS_MSG_DEVICE_BUSY_SWITCH_FAILED", + "text": "Das Gerät ist belegt, Wechsel fehlgeschlagen!" + }, + { + "id": "IDS_MSG_DEVICE_NO_DRIVER", + "text": "Der Gerätetreiber ist möglicherweise nicht installiert!" + }, + { + "id": "IDS_MSG_NO_ENABLED_CHANNEL", + "text": "Keine Kanäle aktiviert!" + }, + { + "id": "IDS_MSG_FILE_NOT_EXIST", + "text": "Datei existiert nicht!" + }, + { + "id": "IDS_MSG_TO_CLEAR_LOG", + "text": "Protokolldatei wirklich löschen?" + }, + { + "id": "IDS_MSG_DEVICE_SPEED_TOO_LOW", + "text": "Fehler: USB-Port-Geschwindigkeit ist zu niedrig (<480 Mbit/s)!" + }, + { + "id": "IDS_MSG_FIRMWARE_NOT_EXIST", + "text": "Fehler: Firmware-Datei existiert nicht!" + }, + { + "id": "IDS_MSG_DEVICE_USB_IO_ERROR", + "text": "Fehler: USB-IO-Fehler!" + }, + { + "id": "IDS_MSG_DATA_RANGE_ERROR", + "text": "Datenbereich ist fehlerhaft!" + }, + { + "id": "IDS_MSG_DATA_RANGE_HAVE_NO_DATA", + "text": "Keine Daten im Datenbereich!" + }, + { + "id": "IDS_MSG_NO_DECODED_RESULT", + "text": "Keine Daten zum Exportieren!" + } +] diff --git a/lang/de/toolbar.json b/lang/de/toolbar.json new file mode 100644 index 000000000..bbea75dbd --- /dev/null +++ b/lang/de/toolbar.json @@ -0,0 +1,198 @@ +[ + { + "id": "IDS_DEVICE_MODE_LOGIC", + "text": "&Logikanalysator" + }, + { + "id": "IDS_DEVICE_MODE_ANALOG", + "text": "&Datenerfassung" + }, + { + "id": "IDS_DEVICE_MODE_DSO", + "text": "&Oszilloskop" + }, + { + "id": "IDS_TOOLBAR_DEVICE_TYPE_DEMO", + "text": "Demo" + }, + { + "id": "IDS_TOOLBAR_DEVICE_TYPE_FILE", + "text": "Datei" + }, + { + "id": "IDS_TOOLBAR_DEVICE_OPTION", + "text": "&Optionen" + }, + { + "id": "IDS_TOOLBAR_CAPTURE_MODE", + "text": "&Modus" + }, + { + "id": "IDS_TOOLBAR_CAPTURE_MODE_SINGLE", + "text": "&Einzeln" + }, + { + "id": "IDS_TOOLBAR_CAPTURE_MODE_REPEAT", + "text": "&Wiederholen" + }, + { + "id": "IDS_TOOLBAR_CAPTURE_MODE_LOOP", + "text": "&Endlos" + }, + { + "id": "IDS_TOOLBAR_RUN_START", + "text": "&Starten" + }, + { + "id": "IDS_TOOLBAR_RUN_STOP", + "text": "S&topp" + }, + { + "id": "IDS_TOOLBAR_ONE_SINGLE", + "text": "E&inzel" + }, + { + "id": "IDS_TOOLBAR_ONE_INSTANT", + "text": "&Sofort" + }, + { + "id": "IDS_TOOLBAR_ONE_STOP", + "text": "S&topp" + }, + { + "id": "IDS_TOOLBAR_TRIGGER", + "text": "&Trigger" + }, + { + "id": "IDS_TOOLBAR_DECODE", + "text": "&Dekodieren" + }, + { + "id": "IDS_TOOLBAR_MEASURE", + "text": "&Messen" + }, + { + "id": "IDS_TOOLBAR_SEARCH", + "text": "&Suchen" + }, + { + "id": "IDS_TOOLBAR_FUNCTION", + "text": "&Funktion" + }, + { + "id": "IDS_TOOLBAR_FUNCTION_FFT", + "text": "&FFT" + }, + { + "id": "IDS_TOOLBAR_FUNCTION_MATH", + "text": "&Mathematik" + }, + { + "id": "IDS_TOOLBAR_DISPLAY", + "text": "&Anzeige" + }, + { + "id": "IDS_TOOLBAR_DISPLAY_THEMES", + "text": "&Designs" + }, + { + "id": "IDS_TOOLBAR_DISPLAY_LISSAJOUS", + "text": "&Lissajous" + }, + { + "id": "IDS_TOOLBAR_DISPLAY_THEMES_DARK", + "text": "&Dunkel" + }, + { + "id": "IDS_TOOLBAR_DISPLAY_THEMES_LIGHT", + "text": "&Hell" + }, + { + "id": "IDS_TOOLBAR_DISPLAY_THEMES_LATTE", + "text": "La&tte" + }, + { + "id": "IDS_TOOLBAR_DISPLAY_THEMES_FRAPPE", + "text": "Fra&ppé" + }, + { + "id": "IDS_TOOLBAR_DISPLAY_OPTIONS", + "text": "&Optionen" + }, + { + "id": "IDS_TOOLBAR_FILE", + "text": "&Datei" + }, + { + "id": "IDS_TOOLBAR_FILE_CONFIG", + "text": "&Konfiguration" + }, + { + "id": "IDS_TOOLBAR_FILE_CONFIG_LOAD", + "text": "&Sitzung laden" + }, + { + "id": "IDS_TOOLBAR_FILE_CONFIG_STORE", + "text": "&Sitzung speichern" + }, + { + "id": "IDS_TOOLBAR_FILE_CONFIG_DEFAULT", + "text": "&Standardsitzung laden" + }, + { + "id": "IDS_TOOLBAR_FILE_OPEN", + "text": "&Öffnen..." + }, + { + "id": "IDS_TOOLBAR_FILE_SAVE", + "text": "&Speichern..." + }, + { + "id": "IDS_TOOLBAR_FILE_EXPORT", + "text": "&Exportieren..." + }, + { + "id": "IDS_TOOLBAR_FILE_CAPTURE", + "text": "&Aufnehmen..." + }, + { + "id": "IDS_TOOLBAR_HELP", + "text": "&Hilfe" + }, + { + "id": "IDS_TOOLBAR_HELP_LANG", + "text": "&Sprache" + }, + { + "id": "IDS_TOOLBAR_HELP_LANG_EN", + "text": "&Englisch" + }, + { + "id": "IDS_TOOLBAR_HELP_LANG_CN", + "text": "Chinesisch(&C)" + }, + { + "id": "IDS_TOOLBAR_HELP_LANG_DE", + "text": "&Deutsch" + }, + { + "id": "IDS_TOOLBAR_HELP_ABOUT", + "text": "&Über..." + }, + { + "id": "IDS_TOOLBAR_HELP_MANUAL", + "text": "&Handbuch..." + }, + { + "id": "IDS_TOOLBAR_HELP_BUG", + "text": "&Fehler melden" + }, + { + "id": "IDS_TOOLBAR_HELP_UPDATE", + "text": "&Aktualisieren" + }, + { + "id": "IDS_TOOLBAR_HELP_LOG", + "text": "&Protokolloptionen" + } +] diff --git a/lang/en/dlg.json b/lang/en/dlg.json index 2959ac107..da90e9402 100644 --- a/lang/en/dlg.json +++ b/lang/en/dlg.json @@ -703,6 +703,10 @@ "id": "IDS_DLG_DISPLAY_ANTIALIAS", "text": "Antialiasing" }, + { + "id": "IDS_DLG_DONT_ASK_SAVE_ON_EXIT", + "text": "Do not ask to save captured data" + }, { "id": "IDS_DLG_SERIAL_HEX", "text": "Hex :" @@ -810,5 +814,33 @@ { "id": "IDS_FFT_MODE_LINEARRSM", "text": "Linear RMS" + }, + { + "id": "IDS_DLG_DECODER_DYNAMIC_FONT_WIDTH", + "text": "Decoder adaptive font width" + }, + { + "id": "IDS_DLG_DEFAULT_FONT", + "text": "Default" + }, + { + "id": "IDS_DLG_MAX_FONT_WIDTH", + "text": "Max font width:" + }, + { + "id": "IDS_DLG_MIN_FONT_WIDTH", + "text": "Min font width:" + }, + { + "id": "IDS_DLG_VERTICAL_SCROLL_ACTION", + "text": "Vertical Scroll Action" + }, + { + "id": "IDS_DLG_VERTICAL_SCROLL_ACTION_SMOOTH_ZOOM", + "text": "Zoom" + }, + { + "id": "IDS_DLG_VERTICAL_SCROLL_ACTION_VERTICAL_SCROLL", + "text": "Scroll" } ] diff --git a/lang/en/toolbar.json b/lang/en/toolbar.json index ed511f43e..19ff22708 100644 --- a/lang/en/toolbar.json +++ b/lang/en/toolbar.json @@ -106,7 +106,15 @@ { "id": "IDS_TOOLBAR_DISPLAY_THEMES_LIGHT", "text": "&Light" - }, + }, + { + "id": "IDS_TOOLBAR_DISPLAY_THEMES_LATTE", + "text": "La&tte" + }, + { + "id": "IDS_TOOLBAR_DISPLAY_THEMES_FRAPPE", + "text": "Fra&ppé" + }, { "id": "IDS_TOOLBAR_DISPLAY_OPTIONS", "text": "&Options" @@ -162,7 +170,11 @@ { "id": "IDS_TOOLBAR_HELP_LANG_CN", "text": "中文(&C)" - }, + }, + { + "id": "IDS_TOOLBAR_HELP_LANG_DE", + "text": "&Deutsch" + }, { "id": "IDS_TOOLBAR_HELP_ABOUT", "text": "&About..." diff --git a/libsigrok4DSL/hardware/DSL/dscope.c b/libsigrok4DSL/hardware/DSL/dscope.c index 1b5d1c01f..af9179b93 100644 --- a/libsigrok4DSL/hardware/DSL/dscope.c +++ b/libsigrok4DSL/hardware/DSL/dscope.c @@ -208,10 +208,11 @@ static GSList *scan(GSList *options) num = 0; is_speed_not_match = 0; - if (options != NULL) + if (options != NULL) { sr_info("Scan DSCope device with options."); - else + } else { sr_info("Scan DSCope device..."); + } conn = NULL; for (l = options; l; l = l->next) { @@ -429,7 +430,7 @@ static uint64_t dso_preoff(const struct sr_channel* ch) static uint64_t dso_offset(const struct sr_dev_inst *sdi, const struct sr_channel* ch) { uint64_t pwm_off = 0; - int offset_coarse, offset_fine; + int offset_coarse = 0, offset_fine = 0; int trans_coarse, trans_fine; struct DSL_context *devc = sdi->priv; const double offset_mid = (1 << (ch->bits - 1)); @@ -1853,7 +1854,7 @@ static int dev_open(struct sr_dev_inst *sdi) gboolean fpga_done; int ret; GSList *l; - gboolean zeroed; + gboolean zeroed = TRUE; struct DSL_context *devc = sdi->priv; if ((ret = dsl_dev_open(di, sdi, &fpga_done)) == SR_OK) { diff --git a/libsigrok4DSL/hardware/DSL/dslogic.c b/libsigrok4DSL/hardware/DSL/dslogic.c index 2382ba9f3..52f48846e 100644 --- a/libsigrok4DSL/hardware/DSL/dslogic.c +++ b/libsigrok4DSL/hardware/DSL/dslogic.c @@ -296,10 +296,11 @@ static GSList *scan(GSList *options) num = 0; is_speed_not_match = 0; - if (options != NULL) + if (options != NULL) { sr_info("Scan DSLogic device with options."); - else + } else { sr_info("Scan DSLogic device..."); + } conn = NULL; for (l = options; l; l = l->next) { diff --git a/libsigrok4DSL/hardware/demo/demo.c b/libsigrok4DSL/hardware/demo/demo.c index 877ae31d1..895e0af27 100644 --- a/libsigrok4DSL/hardware/demo/demo.c +++ b/libsigrok4DSL/hardware/demo/demo.c @@ -72,13 +72,13 @@ static int b_load_directory = 0; static char* demo_mode_names[3] = {"logic", "dso", "analog"}; static const struct DEMO_channels logic_channel_modes[] = { - {DEMO_LOGIC125x16, LOGIC, SR_CHANNEL_LOGIC, 16, 1, SR_MHZ(1), SR_Mn(1), + {(enum DEMO_CHANNEL_ID)DEMO_LOGIC125x16, LOGIC, SR_CHANNEL_LOGIC, 16, 1, SR_MHZ(1), SR_Mn(1), SR_KHZ(50), SR_MHZ(125), "Use 16 Channels (Max 125MHz)"}, - {DEMO_LOGIC250x12, LOGIC, SR_CHANNEL_LOGIC, 12, 1, SR_MHZ(1), SR_Mn(1), + {(enum DEMO_CHANNEL_ID)DEMO_LOGIC250x12, LOGIC, SR_CHANNEL_LOGIC, 12, 1, SR_MHZ(1), SR_Mn(1), SR_KHZ(50), SR_MHZ(250), "Use 12 Channels (Max 250MHz)"}, - {DEMO_LOGIC500x6, LOGIC, SR_CHANNEL_LOGIC, 6, 1, SR_MHZ(1), SR_Mn(1), + {(enum DEMO_CHANNEL_ID)DEMO_LOGIC500x6, LOGIC, SR_CHANNEL_LOGIC, 6, 1, SR_MHZ(1), SR_Mn(1), SR_KHZ(50), SR_MHZ(500), "Use 6 Channels (Max 500MHz)"}, - {DEMO_LOGIC1000x3, LOGIC, SR_CHANNEL_LOGIC, 3, 1, SR_MHZ(1), SR_Mn(1), + {(enum DEMO_CHANNEL_ID)DEMO_LOGIC1000x3, LOGIC, SR_CHANNEL_LOGIC, 3, 1, SR_MHZ(1), SR_Mn(1), SR_KHZ(50), SR_GHZ(1), "Use 3 Channels (Max 1GHz)"}, }; @@ -474,6 +474,9 @@ static void scan_dsl_file(struct sr_dev_inst *sdi) { struct session_vdev * vdev = sdi->priv; int dex; + int init_mode; + const char *default_file; + uint64_t mode_caps; if (b_load_directory == 0) { @@ -483,14 +486,31 @@ static void scan_dsl_file(struct sr_dev_inst *sdi) b_load_directory = 1; } - dex = get_pattern_mode_index_by_string(LOGIC, DEFAULT_LOGIC_FILE); + /* Pick the device's own default mode, so devices that don't support + * LOGIC mode (e.g. a DSO-only demo profile) don't get forced into it. */ + mode_caps = (vdev->profile != NULL) ? vdev->profile->dev_caps.mode_caps : CAPS_MODE_LOGIC; + + if (mode_caps & CAPS_MODE_LOGIC){ + init_mode = LOGIC; + default_file = DEFAULT_LOGIC_FILE; + } + else if (mode_caps & CAPS_MODE_DSO){ + init_mode = DSO; + default_file = DEFAULT_DSO_FILE; + } + else{ + init_mode = ANALOG; + default_file = DEFAULT_ANALOG_FILE; + } + + dex = get_pattern_mode_index_by_string(init_mode, default_file); if(dex == -1){ dex = PATTERN_RANDOM; } vdev->sample_generator = dex; - sdi->mode = LOGIC; + sdi->mode = init_mode; reset_dsl_path(sdi, dex); } @@ -636,50 +656,63 @@ static GSList *hw_scan(GSList *options) struct sr_dev_inst *sdi; struct session_vdev *vdev; GSList *devices; + int i; + int init_mode; (void)options; devices = NULL; - vdev = g_try_malloc0(sizeof(struct session_vdev)); - if (vdev == NULL) + for (i = 0; supported_Demo[i].vendor != 0; i++) { - sr_err("%s: sdi->priv malloc failed", __func__); - return devices; - } - memset(vdev, 0, sizeof(struct session_vdev)); + vdev = g_try_malloc0(sizeof(struct session_vdev)); + if (vdev == NULL) + { + sr_err("%s: sdi->priv malloc failed", __func__); + continue; + } + memset(vdev, 0, sizeof(struct session_vdev)); - sdi = sr_dev_inst_new(LOGIC, SR_ST_INACTIVE, - supported_Demo[0].vendor, - supported_Demo[0].model, - supported_Demo[0].model_version); - if (!sdi) - { - safe_free(vdev); - sr_err("Device instance creation failed."); - return NULL; - } + init_mode = (supported_Demo[i].dev_caps.mode_caps & CAPS_MODE_LOGIC) ? LOGIC : + (supported_Demo[i].dev_caps.mode_caps & CAPS_MODE_DSO) ? DSO : ANALOG; + + sdi = sr_dev_inst_new(init_mode, SR_ST_INACTIVE, + supported_Demo[i].vendor, + supported_Demo[i].model, + supported_Demo[i].model_version); + if (!sdi) + { + safe_free(vdev); + sr_err("Device instance creation failed."); + continue; + } + + vdev->profile = &supported_Demo[i]; - sdi->priv = vdev; - sdi->driver = di; - sdi->dev_type = DEV_TYPE_DEMO; + sdi->priv = vdev; + sdi->driver = di; + sdi->dev_type = DEV_TYPE_DEMO; - vdev->is_loop = 0; + vdev->is_loop = 0; - devices = g_slist_append(devices, sdi); + devices = g_slist_append(devices, sdi); + } return devices; } static const GSList *hw_dev_mode_list(const struct sr_dev_inst *sdi) { - (void)sdi; - + struct session_vdev *vdev; + const struct DEMO_profile *profile; GSList *l = NULL; unsigned int i; - for (i = 0; i < ARRAY_SIZE(sr_mode_list); i++) + vdev = (sdi != NULL) ? sdi->priv : NULL; + profile = (vdev != NULL && vdev->profile != NULL) ? vdev->profile : &supported_Demo[0]; + + for (i = 0; i < ARRAY_SIZE(sr_mode_list); i++) { - if (supported_Demo[0].dev_caps.mode_caps & (1 << i)){ + if (profile->dev_caps.mode_caps & (1 << i)){ l = g_slist_append(l, (gpointer)&sr_mode_list[i]); } } @@ -1135,7 +1168,7 @@ static int config_set(int id, GVariant *data, struct sr_dev_inst *sdi, if(logic_channel_modes[i].id == (enum DEMO_CHANNEL_ID)nv) { vdev->logic_ch_mode_index = i; - vdev->logic_ch_mode = (enum DEMO_CHANNEL_ID)nv; + vdev->logic_ch_mode = (enum DEMO_LOGIC_CHANNEL_ID)nv; load_virtual_device_session(sdi); vdev->channel_mode_change = TRUE; break; diff --git a/libsigrok4DSL/hardware/demo/demo.h b/libsigrok4DSL/hardware/demo/demo.h index 4d8753577..a0ef9bc27 100644 --- a/libsigrok4DSL/hardware/demo/demo.h +++ b/libsigrok4DSL/hardware/demo/demo.h @@ -233,6 +233,8 @@ struct session_vdev enum DEMO_LOGIC_CHANNEL_INDEX logic_ch_mode_index; int is_loop; + + const struct DEMO_profile *profile; }; #define SESSION_MAX_CHANNEL_COUNT 512 @@ -412,9 +414,27 @@ static const gboolean default_ms_en[] = { static const struct DEMO_profile supported_Demo[] = { /* - * Demo + * Demo Oscilloscope + */ + {"DreamSourceLab", "Demo Oscilloscope", NULL, + {CAPS_MODE_DSO, + CAPS_FEATURE_NONE, + (1 << DEMO_DSO200x2), + SR_Kn(20), + SR_Kn(20), + 0, + vdivs10to2000, + 0, + DEMO_DSO200x2, + PATTERN_RANDOM, + SR_NS(500)} + }, + + /* + * Demo Logic (listed last so it is the default device selected at launch; + * see SigSession::set_default_device(), which picks the last device). */ - {"DreamSourceLab", "Demo Device", NULL, + {"DreamSourceLab", "Demo Logic", NULL, {CAPS_MODE_LOGIC | CAPS_MODE_ANALOG | CAPS_MODE_DSO, CAPS_FEATURE_NONE, (1 << DEMO_LOGIC100x16) | @@ -425,7 +445,7 @@ static const struct DEMO_profile supported_Demo[] = { 0, vdivs10to2000, 0, - DEMO_LOGIC100x16, + DEMO_LOGIC100x16, PATTERN_RANDOM, SR_NS(500)} }, diff --git a/libsigrok4DSL/output/csv.c b/libsigrok4DSL/output/csv.c index 0bff430b6..09bb21155 100644 --- a/libsigrok4DSL/output/csv.c +++ b/libsigrok4DSL/output/csv.c @@ -70,7 +70,6 @@ static int init(struct sr_output *o, GHashTable *options) GSList *l; int i; float range; - int ch_num; if (!o || !o->sdi) return SR_ERR_ARG; @@ -87,7 +86,6 @@ static int init(struct sr_output *o, GHashTable *options) ctx->mask = 0; ctx->index = 0; ctx->type = g_variant_get_int16(g_hash_table_lookup(options, "type")); - ch_num = 0; if (o->start_sample_index > 0){ ctx->index = o->start_sample_index; @@ -147,16 +145,14 @@ static GString *gen_header(const struct sr_output *o) struct sr_channel *ch; GString *header; GSList *l; - time_t t; int num_channels, i; ctx = o->priv; header = g_string_sized_new(512); /* Some metadata */ - t = time(NULL); g_string_append_printf(header, "; CSV, generated by DSView, sampled on %s\n", - o->time_string); //ctime(&t) + o->time_string); /* Columns / channels */ if (ctx->type == SR_CHANNEL_LOGIC) @@ -298,7 +294,7 @@ static int receive(const struct sr_output *o, const struct sr_datafeed_packet *p pFlagCheck++; } - g_string_append_printf(*out, tmp_buffer); + g_string_append_printf(*out, "%s", tmp_buffer); } for (j = 0; j < ctx->num_enabled_channels; j++) { @@ -365,7 +361,7 @@ static int receive(const struct sr_output *o, const struct sr_datafeed_packet *p for (i = 0; i < (uint64_t)analog->num_samples; i++) { ch_cfg_dex = 0; - for (j = 0; j < ch_num; j++) { + for (j = 0; j < (uint64_t)ch_num; j++) { if (enalbe_channel_flags[j] == 0){ continue; diff --git a/libsigrokdecode4DSL/decoders/mcp230xx/__init__.py b/libsigrokdecode4DSL/decoders/mcp230xx/__init__.py new file mode 100644 index 000000000..cee4c79e3 --- /dev/null +++ b/libsigrokdecode4DSL/decoders/mcp230xx/__init__.py @@ -0,0 +1,25 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2019 Benedikt Otto +## +## This program is free software; you can 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 . +## + +''' +This decoder stacks on top of the 'i2c' PD and decodes the Microchip +8-bit MCP23008 and 16-bit MCP23017 I²C output expander protocol. +''' + +from .pd import Decoder diff --git a/libsigrokdecode4DSL/decoders/mcp230xx/pd.py b/libsigrokdecode4DSL/decoders/mcp230xx/pd.py new file mode 100644 index 000000000..58a1bce6d --- /dev/null +++ b/libsigrokdecode4DSL/decoders/mcp230xx/pd.py @@ -0,0 +1,143 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2019 Benedikt Otto +## +## This program is free software; you can 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 . +## + +import sigrokdecode as srd + +STATE_IDLE, STATE_ADDR, STATE_DATA, STATE_READ_ADDR, STATE_READ_DATA, STATE_STOP = range(6) +UNKNOWN, READ, WRITE = range(3) + +registers = ["IODIR", "IPOL", "GPINTEN", "DEFVAL", "INTCON", "IOCON", "GPPU", "INTF", "INTCAP", "GPIO", "OLAT"] +registers_mcp23017_bank0 = {i: (registers[i // 2] + "AB"[i % 2] if registers[i // 2] != "IOCON" else "IOCON") for i in range(22)} + +registers_mcp23017_bank1 = {(i + 5 if i > 11 else i): (registers[i % 11] + "AB"[i // 11] if registers[i % 11] != "IOCON" else "IOCON") for i in range(22)} + +registers_mcp23008 = {i: registers[i] for i in range(11)} + +class Decoder(srd.Decoder): + api_version = 3 + id = 'mcp230xx' + name = 'MCP230XX' + longname = 'Microchip MCP230XX' + desc = 'MCP230XX 8/16-bit I²C output expanders.' + license = 'gplv2+' + inputs = ['i2c'] + outputs = [] + tags = ['IC'] + + options = ( + {'id': 'type', 'desc': 'Type', 'default': 'MCP23017', + 'values': ('MCP23008', 'MCP23017')}, + ) + + annotations = ( + ('register_read', 'Register read'), + ('register_write', 'Register write'), + ('warning', 'Warning'), + ) + annotation_rows = ( + ('regs', 'Registers', (0, 1)), + ('warnings', 'Warnings', (2,)), + ) + + def __init__(self): + self.reset() + + def reset(self): + self.state = STATE_IDLE + self.iocon = 0 + self.iocon_set = False + + def start(self): + self.out_ann = self.register(srd.OUTPUT_ANN) + + def get_registers(self): + if self.options["type"] == "MCP23008": + return registers_mcp23008 + else: + return registers_mcp23017_bank1 if self.iocon & (1 << 7) else registers_mcp23017_bank0 + + def putx(self, ss, es, data): + self.put(ss, es, self.out_ann, data) + + def checkAddress(self, ss, es, address): + if not address in range(0x20, 0x27 + 1): + self.putx(ss, es, [2, ['Address %02X not MCP230XX compatible' % address]]) + + def handleRead(self, register, data): + if len(data) >= 1: + register = register[0] + for d in data: + registers = self.get_registers() + if not register in registers: + self.putx(d[1], d[2], [2, ['Error: Register %d not accessible' %register]]) + if not self.iocon_set: + self.iocon = d[0] + else: + register_name = registers[register] + if register_name == "IOCON": + self.iocon = d[0] + self.iocon_set = True + self.putx(d[1], d[2], [0, ["Read %s: %02X" % (register_name, d[0]), "R%02X" % d[0]]]) + register += 1 + + def handleWrite(self, data): + if len(data) >= 2: + register = data[0][0] + for d in data[1:]: + registers = self.get_registers() + if not register in registers: + self.putx(d[1], d[2], [2, ['Error: Register %d not accessible' %register]]) + if not self.iocon_set: + self.iocon = d[0] + else: + register_name = registers[register] + if register_name == "IOCON": + self.iocon = d[0] + self.iocon_set = True + self.putx(d[1], d[2], [1, ["Write %s: %02X" % (register_name, d[0]), "W%02X" % d[0]]]) + register += 1 + + def decode(self, ss, es, data): + cmd, databyte = data + if cmd in ('ACK', 'NACK', 'BITS'): # Discard 'ACK' and 'BITS'. + return + if self.state == STATE_IDLE and cmd == 'START': + self.state = STATE_ADDR + self.dataWrite = [] + self.dataRead = [] + elif self.state == STATE_ADDR and cmd == 'ADDRESS WRITE': + self.state = STATE_DATA + self.checkAddress(ss, es, databyte) + elif self.state in [STATE_DATA, STATE_STOP] and cmd == 'DATA WRITE': + self.state = STATE_STOP + self.dataWrite.append((databyte, ss, es)) + elif self.state == STATE_STOP and cmd == "START REPEAT": + self.state = STATE_READ_ADDR + elif self.state == STATE_READ_ADDR and cmd == "ADDRESS READ": + self.state = STATE_READ_DATA + self.checkAddress(ss, es, databyte) + elif self.state in [STATE_READ_DATA, STATE_STOP] and cmd == 'DATA READ': + self.state = STATE_STOP + self.dataRead.append((databyte, ss, es)) + elif self.state == STATE_STOP and cmd == 'STOP': + self.state = STATE_IDLE + if len(self.dataRead) > 0 and len(self.dataWrite) == 1: + self.handleRead(self.dataWrite[0], self.dataRead) + elif len(self.dataWrite) > 0 and self.dataRead == []: + self.handleWrite(self.dataWrite) diff --git a/libsigrokdecode4DSL/decoders/tmp112/__init__.py b/libsigrokdecode4DSL/decoders/tmp112/__init__.py new file mode 100644 index 000000000..830c10431 --- /dev/null +++ b/libsigrokdecode4DSL/decoders/tmp112/__init__.py @@ -0,0 +1,25 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2026 Patrick Felixberger +## +## This program is free software; you can 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 . +## + +''' +This decoder stacks on top of the 'i2c' PD and decodes the Texas +Instruments TMP112 digital temperature sensor protocol. +''' + +from .pd import Decoder diff --git a/libsigrokdecode4DSL/decoders/tmp112/pd.py b/libsigrokdecode4DSL/decoders/tmp112/pd.py new file mode 100644 index 000000000..1d73c3a78 --- /dev/null +++ b/libsigrokdecode4DSL/decoders/tmp112/pd.py @@ -0,0 +1,257 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2026 Patrick Felixberger +## +## This program is free software; you can 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 . +## + +# Texas Instruments TMP112 digital temperature sensor (datasheet SBOS344H). +# +# TMP112 uses a pointer register scheme: a WRITE transaction's first data +# byte selects one of four 16-bit registers (Temperature, Configuration, +# T_LOW, T_HIGH); any following data bytes in that same WRITE transaction +# are the MSB/LSB of the selected register's new value. A READ transaction +# has no pointer byte of its own -- it returns the 16-bit value of whatever +# register the pointer is currently set to (last one selected by a WRITE, +# or the Temperature register at power-up). + +import sigrokdecode as srd + +REG_TEMPERATURE = 0x00 +REG_CONFIG = 0x01 +REG_TLOW = 0x02 +REG_THIGH = 0x03 + +reg_name = { + REG_TEMPERATURE: 'Temperature', + REG_CONFIG: 'Configuration', + REG_TLOW: 'T_LOW', + REG_THIGH: 'T_HIGH', +} + +fault_queue = { + 0b00: 1, + 0b01: 2, + 0b10: 4, + 0b11: 6, +} + +conversion_rate = { + 0b00: '0.25 Hz', + 0b01: '1 Hz', + 0b10: '4 Hz', + 0b11: '8 Hz', +} + + +def twos_complement(val, bits): + if val & (1 << (bits - 1)): + val -= (1 << bits) + return val + + +class Decoder(srd.Decoder): + api_version = 3 + id = 'tmp112' + name = 'TMP112' + longname = 'Texas Instruments TMP112' + desc = 'Digital I2C temperature sensor with pointer-register addressing.' + license = 'gplv2+' + inputs = ['i2c'] + outputs = [] + tags = ['Sensor', 'IC'] + annotations = ( + ('temperature', 'Temperature'), + ('temperature-verbose', 'Temperature (verbose)'), + ('config', 'Configuration (verbose)'), + ('config-short', 'Configuration'), + ('reg-select', 'Register select'), + ('warnings', 'Warnings'), + ) + annotation_rows = ( + ('temperature', 'Temperature', (0, 1)), + ('config', 'Configuration', (2, 3)), + ('reg-select', 'Register select', (4,)), + ('warnings', 'Warnings', (5,)), + ) + + def __init__(self): + self.reset() + + def reset(self): + self.state = 'IDLE' + self.reg = REG_TEMPERATURE # Pointer register defaults to 0x00 at power-up. + self.extended_mode = False # EM bit shadow, tracked from Config register traffic. + self.rw = None + self.wbytes = [] + self.rbytes = [] + + def start(self): + self.out_ann = self.register(srd.OUTPUT_ANN) + + def putx(self, ss, es, data): + self.put(ss, es, self.out_ann, data) + + def putb(self, data): + self.put(self.ss_block, self.es_block, self.out_ann, data) + + def warn_upon_invalid_slave(self, ss, es, addr): + # ADD0 pin selects the 7-bit address: GND=0x48, V+=0x49, SDA=0x4A, SCL=0x4B. + # The upstream i2c decoder's 'address_format' option controls whether + # ADDRESS WRITE/READ deliver the bare 7-bit address (shifted) or the + # full byte with the R/W bit still packed in at bit 0 (unshifted, + # the default) -- accept either encoding here. + shifted = addr in range(0x48, 0x4B + 1) + unshifted = addr in range(0x48 << 1, (0x4B << 1) + 2) + if not (shifted or unshifted): + s = 'Warning: I2C slave 0x%02x not a TMP112 compatible address.' + self.putx(ss, es, [5, [s % addr]]) + + def decode_temperature_value(self, raw16): + if self.extended_mode: + value = twos_complement(raw16 >> 3, 13) + else: + value = twos_complement(raw16 >> 4, 12) + return value * 0.0625 + + def output_temperature_reg(self, label, raw16): + celsius = self.decode_temperature_value(raw16) + fahrenheit = celsius * 9.0 / 5.0 + 32.0 + self.putb([0, ['%s: %.4f degC' % (label, celsius), + '%.4f degC' % celsius]]) + self.putb([1, ['%s: %.4f degC (%.4f degF, raw=0x%04x)' % + (label, celsius, fahrenheit, raw16)]]) + + def output_config_reg(self, raw16, from_write): + os_bit = (raw16 >> 15) & 1 + fq = (raw16 >> 11) & 0b11 + pol = (raw16 >> 10) & 1 + tm = (raw16 >> 9) & 1 + sd = (raw16 >> 8) & 1 + cr = (raw16 >> 6) & 0b11 + al = (raw16 >> 5) & 1 + em = raw16 & 1 + + self.extended_mode = bool(em) + + s = 'OS = %d: %s\n' % (os_bit, 'one-shot armed/converting' if os_bit else 'idle') + s += 'Fault queue: %d consecutive fault(s)\n' % fault_queue[fq] + s += 'ALERT polarity: active-%s\n' % ('high' if pol else 'low') + s += 'Thermostat mode: %s\n' % ('interrupt' if tm else 'comparator') + s += 'SD = %d: %s\n' % (sd, 'shutdown' if sd else 'continuous conversion') + s += 'Conversion rate: %s\n' % conversion_rate[cr] + s += 'AL (alert, read-only) = %d\n' % al + s += 'EM = %d: %s' % (em, 'extended 13-bit mode' if em else 'normal 12-bit mode') + + s2 = 'SD=%s, CR=%s, FQ=%d, POL=%s, TM=%s, EM=%s' % ( + 'shutdown' if sd else 'continuous', + conversion_rate[cr], fault_queue[fq], + 'high' if pol else 'low', + 'interrupt' if tm else 'comparator', + '13bit' if em else '12bit') + + self.putb([2, [s]]) + self.putb([3, [s2]]) + + def output_register_write(self, reg, raw16): + if reg == REG_TEMPERATURE: + self.putb([5, ['Warning: Temperature register is read-only!']]) + self.output_temperature_reg('Temperature (invalid write)', raw16) + elif reg == REG_CONFIG: + self.output_config_reg(raw16, True) + elif reg == REG_TLOW: + self.output_temperature_reg('T_LOW', raw16) + elif reg == REG_THIGH: + self.output_temperature_reg('T_HIGH', raw16) + + def output_register_read(self, reg, raw16): + if reg == REG_CONFIG: + self.output_config_reg(raw16, False) + else: + self.output_temperature_reg(reg_name.get(reg, 'Register 0x%02x' % reg), raw16) + + def decode(self, ss, es, data): + cmd, databyte = data + self.ss, self.es = ss, es + + if self.state == 'IDLE': + if cmd != 'START': + return + self.state = 'ADDR' + + elif self.state == 'ADDR': + if cmd == 'ADDRESS WRITE': + self.warn_upon_invalid_slave(ss, es, databyte) + self.rw = 'WRITE' + self.wbytes = [] + self.state = 'WRITE POINTER' + elif cmd == 'ADDRESS READ': + self.warn_upon_invalid_slave(ss, es, databyte) + self.rw = 'READ' + self.rbytes = [] + self.ss_block = None + self.state = 'READ DATA' + + elif self.state == 'WRITE POINTER': + if cmd == 'DATA WRITE': + self.reg = databyte & 0x03 + self.putx(ss, es, [4, ['Select register: %s (0x%02x)' % + (reg_name.get(self.reg, '?'), self.reg)]]) + self.ss_block = None + self.state = 'WRITE DATA' + elif cmd in ('START REPEAT', 'STOP'): + self.state = 'ADDR' if cmd == 'START REPEAT' else 'IDLE' + + elif self.state == 'WRITE DATA': + if cmd == 'DATA WRITE': + if self.ss_block is None: + self.ss_block = ss + self.wbytes.append(databyte) + self.es_block = es + if len(self.wbytes) == 2: + raw16 = (self.wbytes[0] << 8) | self.wbytes[1] + self.output_register_write(self.reg, raw16) + self.wbytes = [] + elif cmd == 'START REPEAT': + if len(self.wbytes) == 1: + self.putb([5, ['Warning: incomplete register write (1 byte)']]) + self.wbytes = [] + self.state = 'ADDR' + elif cmd == 'STOP': + if len(self.wbytes) == 1: + self.putb([5, ['Warning: incomplete register write (1 byte)']]) + self.wbytes = [] + self.state = 'IDLE' + + elif self.state == 'READ DATA': + if cmd == 'DATA READ': + if self.ss_block is None: + self.ss_block = ss + self.rbytes.append(databyte) + self.es_block = es + if len(self.rbytes) == 2: + raw16 = (self.rbytes[0] << 8) | self.rbytes[1] + self.output_register_read(self.reg, raw16) + self.rbytes = [] + elif cmd == 'START REPEAT': + if len(self.rbytes) == 1: + self.putb([5, ['Warning: incomplete register read (1 byte)']]) + self.rbytes = [] + self.state = 'ADDR' + elif cmd == 'STOP': + if len(self.rbytes) == 1: + self.putb([5, ['Warning: incomplete register read (1 byte)']]) + self.rbytes = [] + self.state = 'IDLE'