# HG changeset patch # User Hasan Yavuz ÖZDERYA # Date 2017-08-26 06:19:16 # Node ID cd91a52b2383ffbd7665f5a203dd0ff10e3fbc1b # Parent 38669ef60576f364433466be628f81a0c12f2c2a # Parent cedcbcc9d86e5b83ea821ef4a6484ca0739f7213 Merge with default diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -9,3 +9,5 @@ b4d0a38444d31872633e474d89ffc15cd0fe42f0 27b0354ca2c5ea7b3870156417ce7e04e799bbf7 v0.7.1 fd5f1eb480ec372b49df58b497458de05c30057c v0.8.0 9c9a11cd15fd094e2b2b65dc51805fd8fd1d2460 v0.8.1 +4cf9a1ee1f107a38e03dbe17c4f2882c43d827c9 v0.9.0 +ef003f7af8f37f760c22dae776f5ff8e1b526deb v0.9.1 diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,7 @@ # along with serialplot. If not, see . # -cmake_minimum_required(VERSION 2.8.11) +cmake_minimum_required(VERSION 3.2.2) project(serialplot) @@ -38,23 +38,19 @@ find_package(Qt5Widgets) # If set, cmake will download Qwt over SVN, build and use it as a static library. set(BUILD_QWT true CACHE BOOL "Download and build Qwt automatically.") -# Find QWT or use static manually provided by user -set(QWT_USE_STATIC false CACHE BOOL "Use a static version of Qwt provided by user.") -set(QWT_STATIC_LIBRARY "" CACHE FILEPATH "Path to the static Qwt library, libqwt.a.") -set(QWT_STATIC_INCLUDE "" CACHE PATH "Path to the Qwt include directory when building Qwt static.") - if (BUILD_QWT) include(BuildQwt) else (BUILD_QWT) - if (QWT_USE_STATIC) - set(QWT_LIBRARY ${QWT_STATIC_LIBRARY}) - set(QWT_INCLUDE_DIR ${QWT_STATIC_INCLUDE}) - else (QWT_USE_STATIC) - find_package(Qwt 6.1 REQUIRED) - endif (QWT_USE_STATIC) + find_package(Qwt 6.1 REQUIRED) endif (BUILD_QWT) -include(BuildQColorWidgets) +# If set, cmake will download QtColorWidgets over git, build and use it as a static library. +set(BUILD_QTCOLORWIDGETS true CACHE BOOL "Download and build QtColorWidgets library automatically.") +if (BUILD_QTCOLORWIDGETS) + include(BuildQColorWidgets) +else () + find_package(QtColorWidgets REQUIRED) +endif () set(BUILD_LEDWIDGET true CACHE BOOL "Download and build LedWidget automatically.") if (BUILD_LEDWIDGET) @@ -64,10 +60,14 @@ else (BUILD_LEDWIDGET) endif (BUILD_LEDWIDGET) # includes -include_directories("./src" ${QWT_INCLUDE_DIR} ${QCW_INCLUDE_DIR} ${LEDWIDGET_INCLUDE_DIR}) +include_directories("./src" + ${QWT_INCLUDE_DIR} + ${QTCOLORWIDGETS_INCLUDE_DIRS} + ${LEDWIDGET_INCLUDE_DIR} + ) # flags -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${QCW_FLAGS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${QTCOLORWIDGETS_FLAGS}") # wrap UI and resource files qt5_wrap_ui(UI_FILES @@ -85,6 +85,7 @@ qt5_wrap_ui(UI_FILES src/binarystreamreadersettings.ui src/asciireadersettings.ui src/framedreadersettings.ui + src/updatecheckdialog.ui ) if (WIN32) @@ -134,24 +135,35 @@ add_executable(${PROGRAM_NAME} WIN32 src/framedreadersettings.cpp src/plotmanager.cpp src/numberformat.cpp + src/updatechecker.cpp + src/versionnumber.cpp + src/updatecheckdialog.cpp misc/windows_icon.rc ${UI_FILES} ${RES_FILES} ) # Use the Widgets module from Qt 5. -target_link_libraries(${PROGRAM_NAME} ${QWT_LIBRARY} ${QCW_LIBRARY} ${LEDWIDGET_LIBRARY}) -qt5_use_modules(${PROGRAM_NAME} Widgets SerialPort) +target_link_libraries(${PROGRAM_NAME} + ${QWT_LIBRARY} + ${QTCOLORWIDGETS_LIBRARIES} + ${LEDWIDGET_LIBRARY} + ) +qt5_use_modules(${PROGRAM_NAME} Widgets SerialPort Network) -# external project dependencies if (BUILD_QWT) add_dependencies(${PROGRAM_NAME} QWT) -endif (BUILD_QWT) +endif () + +if (BUILD_QTCOLORWIDGETS) + add_dependencies(${PROGRAM_NAME} QCW) +endif () if (BUILD_LEDWIDGET) add_dependencies(${PROGRAM_NAME} LEDW) endif (BUILD_LEDWIDGET) + # set compiler flags set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") @@ -167,34 +179,18 @@ else() message(STATUS "The compiler ${CMAKE_CXX_COMPILER} has no C++11 support. Please use a different C++ compiler.") endif() -# version number -set(MAJOR_VERSION 0 CACHE INT "Program major version number.") -set(MINOR_VERSION 8 CACHE INT "Program minor version number.") -set(PATCH_VERSION 1 CACHE INT "Program patch version number.") -set(VERSION_STRING "${MAJOR_VERSION}.${MINOR_VERSION}.${PATCH_VERSION}") - -# get revision number from mercurial -find_program(MERCURIAL hg) +# default version +set(VERSION_STRING "0.9.1") +set(VERSION_REVISION "0") -if (MERCURIAL) - execute_process(COMMAND ${MERCURIAL} id -i - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - RESULT_VARIABLE MERCURIAL_RESULT - OUTPUT_VARIABLE VERSION_REVISION - OUTPUT_STRIP_TRAILING_WHITESPACE) - if(NOT MERCURIAL_RESULT EQUAL 0) - set(VERSION_SCM_REVISION false) - endif(NOT MERCURIAL_RESULT EQUAL 0) -endif (MERCURIAL) +# get revision number from mercurial and parse version string +include(GetVersion) -if (NOT VERSION_REVISION) - set(VERSION_REVISION "0") -endif (NOT VERSION_REVISION) - -message("SCM revision: ${VERSION_REVISION}") - -# configure version file -configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/version.h.in" "${CMAKE_CURRENT_BINARY_DIR}/version.h") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_STRING=\\\"${VERSION_STRING}\\\" ") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_MAJOR=${VERSION_MAJOR} ") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_MINOR=${VERSION_MINOR} ") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_PATCH=${VERSION_PATCH} ") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_REVISION=\\\"${VERSION_REVISION}\\\" ") # add make run target add_custom_target(run @@ -213,20 +209,19 @@ if (WIN32) install(FILES ${WINDOWS_INSTALL_LIBRARIES} DESTINATION bin) endif (WIN32) -# install menu item and icon +# prepare menu item and icon configure_file("${CMAKE_CURRENT_SOURCE_DIR}/misc/program_name.desktop.in" "${CMAKE_BINARY_DIR}/${PROGRAM_NAME}.desktop") configure_file("${CMAKE_CURRENT_SOURCE_DIR}/misc/program_name.png" "${CMAKE_BINARY_DIR}/${PROGRAM_NAME}.png" COPYONLY) +set(DESKTOP_FILE ${CMAKE_BINARY_DIR}/${PROGRAM_NAME}.desktop) +set(ICON_FILE ${CMAKE_BINARY_DIR}/${PROGRAM_NAME}.png) + +# install menu item and icon if (UNIX) - # first copy files to share/serialplot/ - install(FILES - ${CMAKE_BINARY_DIR}/${PROGRAM_NAME}.desktop - DESTINATION share/applications/) - install(FILES - ${CMAKE_BINARY_DIR}/${PROGRAM_NAME}.png - DESTINATION share/icons/hicolor/256x256/apps/) + install(FILES ${DESKTOP_FILE} DESTINATION share/applications/) + install(FILES ${ICON_FILE} DESTINATION share/icons/hicolor/256x256/apps/) endif (UNIX) # uninstalling @@ -241,6 +236,8 @@ if (UNIX) endif (UNIX) # packaging +include(BuildLinuxAppImage) + if (UNIX) set(CPACK_GENERATOR "DEB") elseif (WIN32) @@ -252,9 +249,9 @@ include(InstallRequiredSystemLibraries) set(CPACK_PACKAGE_NAME "${PROGRAM_NAME}") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Small and simple software for plotting data from serial port") set(CPACK_PACKAGE_CONTACT "Hasan Yavuz Özderya ") -set(CPACK_PACKAGE_VERSION_MAJOR ${MAJOR_VERSION}) -set(CPACK_PACKAGE_VERSION_MINOR ${MINOR_VERSION}) -set(CPACK_PACKAGE_VERSION_PATCH ${PATCH_VERSION}) +set(CPACK_PACKAGE_VERSION_MAJOR ${VERSION_MAJOR}) +set(CPACK_PACKAGE_VERSION_MINOR ${VERSION_MINOR}) +set(CPACK_PACKAGE_VERSION_PATCH ${VERSION_PATCH}) set(CPACK_STRIP_FILES TRUE) set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5widgets5 (>= 5.2.1), libqt5svg5 (>= 5.2.1), libqt5serialport5 (>= 5.2.1), libc6 (>= 2.19)") set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "Small and simple software for plotting data from serial port diff --git a/Dockerfile b/Dockerfile new file mode 100644 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM ubuntu:trusty + +WORKDIR /serialplot +ADD . /serialplot + +# Install build dependencies +RUN apt-get update +RUN apt-get -y install software-properties-common +RUN add-apt-repository -y ppa:beineri/opt-qt562-trusty +RUN add-apt-repository -y ppa:george-edison55/cmake-3.x +RUN apt-get update +RUN apt-get -y install build-essential mesa-common-dev qt56base qt56serialport cmake mercurial subversion git wget libfuse2 + +# Define environment variable +ENV PATH /opt/qt56/bin/:$PATH diff --git a/cmake/modules/BuildLinuxAppImage.cmake b/cmake/modules/BuildLinuxAppImage.cmake new file mode 100644 --- /dev/null +++ b/cmake/modules/BuildLinuxAppImage.cmake @@ -0,0 +1,36 @@ +# Based on: https://github.com/mhoeher/opentodolist + +set(LINUXDEPLOYQT_URL "https://github.com/probonopd/linuxdeployqt/releases/download/4/linuxdeployqt-4-x86_64.AppImage") +set(LINUXDEPLOYQT_TOOL ${CMAKE_CURRENT_BINARY_DIR}/linuxdeployqt-4-x86_64.AppImage) + +set(APPIMAGE_DIR ${CMAKE_CURRENT_BINARY_DIR}/${PROGRAM_NAME}-${VERSION_STRING}-${CMAKE_HOST_SYSTEM_PROCESSOR}) + +add_custom_command( + OUTPUT + ${LINUXDEPLOYQT_TOOL} + COMMAND + wget ${LINUXDEPLOYQT_URL} + COMMAND + chmod a+x ${LINUXDEPLOYQT_TOOL}) + +add_custom_target( + appimage + + DEPENDS ${LINUXDEPLOYQT_TOOL} + + COMMAND + ${CMAKE_COMMAND} -E remove_directory ${APPIMAGE_DIR} + COMMAND + ${CMAKE_COMMAND} -E make_directory ${APPIMAGE_DIR} + COMMAND + ${CMAKE_COMMAND} -E copy $ ${APPIMAGE_DIR} + COMMAND + ${CMAKE_COMMAND} -E copy ${DESKTOP_FILE} ${APPIMAGE_DIR} + COMMAND + ${CMAKE_COMMAND} -E copy ${ICON_FILE} ${APPIMAGE_DIR} + COMMAND + ${CMAKE_COMMAND} -E env PATH=${QT_INSTALL_PREFIX}/bin:$ENV{PATH} ${LINUXDEPLOYQT_TOOL} + ${APPIMAGE_DIR}/${PROGRAM_NAME} -appimage + -always-overwrite -bundle-non-qt-libs -verbose=2 + WORKING_DIRECTORY + ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/cmake/modules/BuildQColorWidgets.cmake b/cmake/modules/BuildQColorWidgets.cmake --- a/cmake/modules/BuildQColorWidgets.cmake +++ b/cmake/modules/BuildQColorWidgets.cmake @@ -22,12 +22,15 @@ include(ExternalProject) ExternalProject_Add(QCW PREFIX qcw GIT_REPOSITORY https://github.com/mbasaglia/Qt-Color-Widgets - PATCH_COMMAND patch -t -N -p1 -i ${CMAKE_CURRENT_LIST_DIR}/qt_5_2_moc_creation_namespace_fix.diff + PATCH_COMMAND patch -t -p1 -i ${CMAKE_CURRENT_LIST_DIR}/qt_5_2_moc_creation_namespace_fix.diff CMAKE_CACHE_ARGS "-DCMAKE_CXX_FLAGS:string=-D QTCOLORWIDGETS_STATICALLY_LINKED" UPDATE_COMMAND "" INSTALL_COMMAND "") ExternalProject_Get_Property(QCW binary_dir source_dir) -set(QCW_FLAGS "-D QTCOLORWIDGETS_STATICALLY_LINKED") -set(QCW_LIBRARY ${binary_dir}/libColorWidgets-qt5.a) -set(QCW_INCLUDE_DIR ${source_dir}/include) +set(QTCOLORWIDGETS_FLAGS "-D QTCOLORWIDGETS_STATICALLY_LINKED") +set(QTCOLORWIDGETS_LIBRARY ${binary_dir}/libColorWidgets-qt5.a) +set(QTCOLORWIDGETS_INCLUDE_DIR ${source_dir}/include) + +set(QTCOLORWIDGETS_LIBRARIES ${QTCOLORWIDGETS_LIBRARY}) +set(QTCOLORWIDGETS_INCLUDE_DIRS ${QTCOLORWIDGETS_INCLUDE_DIR}) diff --git a/cmake/modules/FindQtColorWidgets.cmake b/cmake/modules/FindQtColorWidgets.cmake new file mode 100644 --- /dev/null +++ b/cmake/modules/FindQtColorWidgets.cmake @@ -0,0 +1,35 @@ +# +# Copyright © 2017 Hasan Yavuz Özderya +# +# This file is part of serialplot. +# +# serialplot is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# serialplot 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 serialplot. If not, see . +# + +# Note: this script is intended for the debian package created for serialplot. + +find_library(QTCOLORWIDGETS_LIBRARY "libColorWidgets-qt5.a") +find_path(QTCOLORWIDGETS_INCLUDE_DIR "color_preview.hpp" PATHS "/usr/include/qtcolorwidgets/" NO_DEFAULT_PATH) + +mark_as_advanced(QTCOLORWIDGETS_LIBRARY QTCOLORWIDGETS_INCLUDE_DIR) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(QtColorWidgets DEFAULT_MSG QTCOLORWIDGETS_LIBRARY QTCOLORWIDGETS_INCLUDE_DIR) + +if (QTCOLORWIDGETS_FOUND) + set(QTCOLORWIDGETS_FLAGS "-D QTCOLORWIDGETS_STATICALLY_LINKED") + mark_as_advanced(QTCOLORWIDGETS_FLAGS) + set(QTCOLORWIDGETS_LIBRARIES ${QTCOLORWIDGETS_LIBRARY}) + set(QTCOLORWIDGETS_INCLUDE_DIRS ${QTCOLORWIDGETS_INCLUDE_DIR}) +endif (QTCOLORWIDGETS_FOUND) diff --git a/cmake/modules/FindQwt.cmake b/cmake/modules/FindQwt.cmake --- a/cmake/modules/FindQwt.cmake +++ b/cmake/modules/FindQwt.cmake @@ -1,5 +1,5 @@ # -# Copyright © 2015 Hasan Yavuz Özderya +# Copyright © 2017 Hasan Yavuz Özderya # # This file is part of serialplot. # @@ -66,7 +66,7 @@ endif(qwt_roots) if(QWT_ROOT) set(QWT_INCLUDE_DIR "${QWT_ROOT}/include") - find_library(QWT_LIBRARY "qwt" + find_library(QWT_LIBRARY "qwt-qt5" PATHS "${QWT_ROOT}/lib") else (QWT_ROOT) ## Look into system locations @@ -90,7 +90,7 @@ else (QWT_ROOT) endif(qwt_version_string) endif (QWT_INCLUDE_DIR) # look into system locations for lib file - find_library(QWT_LIBRARY "qwt" PATHS /usr/lib) + find_library(QWT_LIBRARY "qwt-qt5" PATHS /usr/lib) endif(QWT_ROOT) # set version variables diff --git a/cmake/modules/GetVersion.cmake b/cmake/modules/GetVersion.cmake new file mode 100644 --- /dev/null +++ b/cmake/modules/GetVersion.cmake @@ -0,0 +1,80 @@ +# +# Copyright © 2017 Hasan Yavuz Özderya +# +# This file is part of serialplot. +# +# serialplot is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# serialplot 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 serialplot. If not, see . +# + +# try to get latest version from mercurial +find_program(HG hg) + +if (HG) + # get latest tag + execute_process(COMMAND ${HG} parents --template {latesttag} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + RESULT_VARIABLE HG_RESULT + OUTPUT_VARIABLE HG_LATEST_TAG + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(HG_RESULT EQUAL 0) + if (NOT HG_LATEST_TAG MATCHES "v[0-9.]+") + unset(HG_LATEST_TAG) + endif() + else() + unset(HG_LATEST_TAG) + endif() + + # get revision + execute_process(COMMAND ${HG} id -i + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + RESULT_VARIABLE HG_RESULT + OUTPUT_VARIABLE HG_REVISION + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT HG_RESULT EQUAL 0) + unset(HG_REVISION) + endif(NOT HG_RESULT EQUAL 0) +endif (HG) + +# Try to get version from .hg_archival file +if (NOT HG_LATEST_TAG) + set(HG_ARCHIVAL_FILE ${CMAKE_CURRENT_SOURCE_DIR}/.hg_archival.txt) + if (EXISTS ${HG_ARCHIVAL_FILE}) + # get latest tag + file(STRINGS ${HG_ARCHIVAL_FILE} HG_ARCHIVAL_LATEST_TAG REGEX "^latesttag:.+") + string(REGEX REPLACE "latesttag:[ \t]*(.+)" "\\1" HG_LATEST_TAG ${HG_ARCHIVAL_LATEST_TAG}) + + # get latest revision + file(STRINGS ${HG_ARCHIVAL_FILE} HG_ARCHIVAL_REV REGEX "^node:.+") + string(REGEX REPLACE "node:[ \t]*([a-fA-F0-9]+)" "\\1" HG_ARCHIVAL_REV ${HG_ARCHIVAL_REV}) + string(SUBSTRING ${HG_ARCHIVAL_REV} 0 12 HG_REVISION) + endif() +endif () + +# extract version information from tag (remove 'v' prefix) +if (HG_LATEST_TAG) + string(REPLACE "v" "" HG_VERSION ${HG_LATEST_TAG}) + message("Version from mercurial: ${HG_VERSION} (${HG_REVISION})") + + # replace version string + set(VERSION_STRING ${HG_VERSION}) + set(VERSION_REVISION ${HG_REVISION}) +else () + message("Failed to find version information from mercurial.") +endif () + +# parse version numbers +string(REPLACE "." ";" VERSION_LIST ${VERSION_STRING}) +list(GET VERSION_LIST 0 VERSION_MAJOR) +list(GET VERSION_LIST 1 VERSION_MINOR) +list(GET VERSION_LIST 2 VERSION_PATCH) diff --git a/serialplot.pro b/serialplot.pro --- a/serialplot.pro +++ b/serialplot.pro @@ -68,7 +68,9 @@ SOURCES += \ src/framedreader.cpp \ src/plotmanager.cpp \ src/numberformat.cpp \ - src/recordpanel.cpp + src/recordpanel.cpp \ + src/updatechecker.cpp \ + src/updatecheckdialog.cpp HEADERS += \ src/mainwindow.h \ @@ -108,7 +110,9 @@ HEADERS += \ src/plotmanager.h \ src/setting_defines.h \ src/numberformat.h \ - src/recordpanel.h + src/recordpanel.h \ + src/updatechecker.h \ + src/updatecheckdialog.h FORMS += \ src/mainwindow.ui \ @@ -124,7 +128,8 @@ FORMS += \ src/framedreadersettings.ui \ src/binarystreamreadersettings.ui \ src/asciireadersettings.ui \ - src/recordpanel.ui + src/recordpanel.ui \ + src/updatecheckdialog.ui INCLUDEPATH += qmake/ src/ diff --git a/src/asciireader.cpp b/src/asciireader.cpp --- a/src/asciireader.cpp +++ b/src/asciireader.cpp @@ -33,6 +33,7 @@ AsciiReader::AsciiReader(QIODevice* devi _numOfChannels = _settingsWidget.numOfChannels(); autoNumOfChannels = (_numOfChannels == NUMOFCHANNELS_AUTO); + delimiter = _settingsWidget.delimiter(); connect(&_settingsWidget, &AsciiReaderSettings::numOfChannelsChanged, [this](unsigned value) @@ -45,6 +46,12 @@ AsciiReader::AsciiReader(QIODevice* devi } }); + connect(&_settingsWidget, &AsciiReaderSettings::delimiterChanged, + [this](QChar d) + { + delimiter = d; + }); + connect(device, &QIODevice::aboutToClose, [this](){discardFirstLine=true;}); } @@ -90,7 +97,7 @@ void AsciiReader::onDataReady() { while(_device->canReadLine()) { - QByteArray line = _device->readLine(); + QString line = QString(_device->readLine()); // discard only once when we just started reading if (discardFirstLine) @@ -116,7 +123,7 @@ void AsciiReader::onDataReady() continue; } - auto separatedValues = line.split(','); + auto separatedValues = line.split(delimiter, QString::SkipEmptyParts); unsigned numReadChannels; // effective number of channels to read unsigned numComingChannels = separatedValues.length(); @@ -143,6 +150,7 @@ void AsciiReader::onDataReady() } // parse read line + unsigned numDataBroken = 0; double* channelSamples = new double[_numOfChannels](); for (unsigned ci = 0; ci < numReadChannels; ci++) { @@ -153,11 +161,15 @@ void AsciiReader::onDataReady() qWarning() << "Data parsing error for channel: " << ci; qWarning() << "Read line: " << line; channelSamples[ci] = 0; + numDataBroken++; } } - // commit data - addData(channelSamples, _numOfChannels); + if (numReadChannels > numDataBroken) + { + // commit data + addData(channelSamples, _numOfChannels); + } delete[] channelSamples; } diff --git a/src/asciireader.h b/src/asciireader.h --- a/src/asciireader.h +++ b/src/asciireader.h @@ -48,6 +48,7 @@ private: unsigned _numOfChannels; /// number of channels will be determined from incoming data unsigned autoNumOfChannels; + QChar delimiter; ///< selected column delimiter bool paused; // We may have (usually true) started reading in the middle of a diff --git a/src/asciireadersettings.cpp b/src/asciireadersettings.cpp --- a/src/asciireadersettings.cpp +++ b/src/asciireadersettings.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -17,20 +17,35 @@ along with serialplot. If not, see . */ +#include +#include + #include "utils.h" #include "setting_defines.h" #include "asciireadersettings.h" #include "ui_asciireadersettings.h" -#include - AsciiReaderSettings::AsciiReaderSettings(QWidget *parent) : QWidget(parent), ui(new Ui::AsciiReaderSettings) { ui->setupUi(this); + auto validator = new QRegularExpressionValidator(QRegularExpression("[^\\d]?"), this); + ui->leDelimiter->setValidator(validator); + + connect(ui->rbComma, &QAbstractButton::toggled, + this, &AsciiReaderSettings::delimiterToggled); + connect(ui->rbSpace, &QAbstractButton::toggled, + this, &AsciiReaderSettings::delimiterToggled); + connect(ui->rbTab, &QAbstractButton::toggled, + this, &AsciiReaderSettings::delimiterToggled); + connect(ui->rbOtherDelimiter, &QAbstractButton::toggled, + this, &AsciiReaderSettings::delimiterToggled); + connect(ui->leDelimiter, &QLineEdit::textChanged, + this, &AsciiReaderSettings::customDelimiterChanged); + // Note: if directly connected we get a runtime warning on incompatible signal arguments connect(ui->spNumOfChannels, SELECT::OVERLOAD_OF(&QSpinBox::valueChanged), [this](int value) @@ -44,11 +59,51 @@ AsciiReaderSettings::~AsciiReaderSetting delete ui; } -unsigned AsciiReaderSettings::numOfChannels() +unsigned AsciiReaderSettings::numOfChannels() const { return ui->spNumOfChannels->value(); } +QChar AsciiReaderSettings::delimiter() const +{ + if (ui->rbComma->isChecked()) + { + return QChar(','); + } + else if (ui->rbSpace->isChecked()) + { + return QChar(' '); + } + else if (ui->rbTab->isChecked()) + { + return QChar('\t'); + } + else // rbOther + { + auto t = ui->leDelimiter->text(); + return t.isEmpty() ? QChar() : t.at(0); + } +} + +void AsciiReaderSettings::delimiterToggled(bool checked) +{ + if (!checked) return; + + auto d = delimiter(); + if (!d.isNull()) + { + emit delimiterChanged(d); + } +} + +void AsciiReaderSettings::customDelimiterChanged(const QString text) +{ + if (ui->rbOtherDelimiter->isChecked()) + { + if (!text.isEmpty()) emit delimiterChanged(text.at(0)); + } +} + void AsciiReaderSettings::saveSettings(QSettings* settings) { settings->beginGroup(SettingGroup_ASCII); @@ -58,6 +113,25 @@ void AsciiReaderSettings::saveSettings(Q if (numOfChannelsSetting == "0") numOfChannelsSetting = "auto"; settings->setValue(SG_ASCII_NumOfChannels, numOfChannelsSetting); + // save delimiter + QString delimiterS; + if (ui->rbOtherDelimiter->isChecked()) + { + delimiterS = "other"; + } + else if (ui->rbTab->isChecked()) + { + // Note: \t is not correctly loaded + delimiterS = "TAB"; + } + else + { + delimiterS = delimiter(); + } + + settings->setValue(SG_ASCII_Delimiter, delimiterS); + settings->setValue(SG_ASCII_CustomDelimiter, ui->leDelimiter->text()); + settings->endGroup(); } @@ -83,5 +157,26 @@ void AsciiReaderSettings::loadSettings(Q } } + // load delimiter + auto delimiterS = settings->value(SG_ASCII_Delimiter, delimiter()).toString(); + auto customDelimiter = settings->value(SG_ASCII_CustomDelimiter, delimiter()).toString(); + if (!customDelimiter.isEmpty()) ui->leDelimiter->setText(customDelimiter); + if (delimiterS == ",") + { + ui->rbComma->setChecked(true); + } + else if (delimiterS == " ") + { + ui->rbSpace->setChecked(true); + } + else if (delimiterS == "TAB") + { + ui->rbTab->setChecked(true); + } + else + { + ui->rbOtherDelimiter->setChecked(true); + } + settings->endGroup(); } diff --git a/src/asciireadersettings.h b/src/asciireadersettings.h --- a/src/asciireadersettings.h +++ b/src/asciireadersettings.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -22,6 +22,7 @@ #include #include +#include namespace Ui { class AsciiReaderSettings; @@ -35,7 +36,8 @@ public: explicit AsciiReaderSettings(QWidget *parent = 0); ~AsciiReaderSettings(); - unsigned numOfChannels(); + unsigned numOfChannels() const; + QChar delimiter() const; /// Stores settings into a `QSettings` void saveSettings(QSettings* settings); /// Loads settings from a `QSettings`. @@ -43,9 +45,15 @@ public: signals: void numOfChannelsChanged(unsigned); + /// Signaled only with a valid delimiter + void delimiterChanged(QChar); private: Ui::AsciiReaderSettings *ui; + +private slots: + void delimiterToggled(bool checked); + void customDelimiterChanged(const QString text); }; #endif // ASCIIREADERSETTINGS_H diff --git a/src/asciireadersettings.ui b/src/asciireadersettings.ui --- a/src/asciireadersettings.ui +++ b/src/asciireadersettings.ui @@ -6,14 +6,17 @@ 0 0 - 414 + 493 171 Form - + + + QFormLayout::ExpandingFieldsGrow + 0 @@ -26,68 +29,105 @@ 0 - + + + + Number Of Channels: + + + + + + + + 60 + 0 + + + + Select number of channels or set to 0 for Auto (determined from incoming data) + + + Auto + + + false + + + 0 + + + 32 + + + + + + + Column Delimiter: + + + + - + - Number Of Channels: + comma + + + true - - - - 60 - 0 - - - - Select number of channels or set to 0 for Auto (determined from incoming data) + + + space - - Auto - - - false + + + + + + tab - - 0 - - - 32 + + + + + + other: - - - Qt::Horizontal + + + + 0 + 0 + - + - 1 - 20 + 30 + 16777215 - + + Enter a custom delimiter character + + + + + + | + + - - - - Qt::Vertical - - - - 20 - 1 - - - - diff --git a/src/channelinfomodel.cpp b/src/channelinfomodel.cpp --- a/src/channelinfomodel.cpp +++ b/src/channelinfomodel.cpp @@ -267,7 +267,7 @@ void ChannelInfoModel::setNumOfChannels( // remember user entered info if ((int) number > infos.length()) { - for (unsigned ci = _numOfChannels; ci < number; ci++) + for (unsigned ci = infos.length(); ci < number; ci++) { infos.append(ChannelInfo(ci)); } diff --git a/src/commandedit.cpp b/src/commandedit.cpp --- a/src/commandedit.cpp +++ b/src/commandedit.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2015 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -62,7 +62,7 @@ CommandEdit::CommandEdit(QWidget *parent QLineEdit(parent) { hexValidator = new HexCommandValidator(this); - asciiValidator = new QRegExpValidator(QRegExp("[\\x0000-\\x007F]+")); + asciiValidator = new QRegExpValidator(QRegExp("[\\x0000-\\x007F]+"), this); ascii_mode = true; setValidator(asciiValidator); } diff --git a/src/commandwidget.cpp b/src/commandwidget.cpp --- a/src/commandwidget.cpp +++ b/src/commandwidget.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -103,7 +103,14 @@ bool CommandWidget::isASCIIMode() void CommandWidget::setASCIIMode(bool enabled) { - ui->pbASCII->setChecked(enabled); + if (enabled) + { + ui->pbASCII->setChecked(true); + } + else + { + ui->pbHEX->setChecked(true); + } } void CommandWidget::setName(QString name) diff --git a/src/datarecorder.cpp b/src/datarecorder.cpp --- a/src/datarecorder.cpp +++ b/src/datarecorder.cpp @@ -27,6 +27,7 @@ DataRecorder::DataRecorder(QObject *pare { lastNumChannels = 0; disableBuffering = false; + windowsLE = false; } bool DataRecorder::startRecording(QString fileName, QString separator, QStringList channelNames) @@ -47,7 +48,7 @@ bool DataRecorder::startRecording(QStrin if (!channelNames.isEmpty()) { fileStream << channelNames.join(_sep); - fileStream << "\n"; + fileStream << le(); lastNumChannels = channelNames.length(); } return true; @@ -74,7 +75,7 @@ void DataRecorder::addData(double* data, fileStream << data[ci * numOfSamples + i]; if (ci != numOfChannels-1) fileStream << _sep; } - fileStream << '\n'; + fileStream << le(); } if (disableBuffering) fileStream.flush(); @@ -87,3 +88,8 @@ void DataRecorder::stopRecording() file.close(); lastNumChannels = 0; } + +const char* DataRecorder::le() const +{ + return windowsLE ? "\r\n" : "\n"; +} diff --git a/src/datarecorder.h b/src/datarecorder.h --- a/src/datarecorder.h +++ b/src/datarecorder.h @@ -34,6 +34,14 @@ public: bool disableBuffering; /** + * Use CR+LF as line ending. `false` by default. + * + * @note Toggling this variable during a recording will result in + * a corrupted file. Care must be taken at higher (UI) levels. + */ + bool windowsLE; + + /** * @brief Starts recording data to a file in CSV format. * * File is opened and header line (names of channels) is written. @@ -72,6 +80,9 @@ private: QFile file; QTextStream fileStream; QString _sep; + + /// Returns the selected line ending. + const char* le() const; }; #endif // DATARECORDER_H diff --git a/src/framebufferseries.cpp b/src/framebufferseries.cpp --- a/src/framebufferseries.cpp +++ b/src/framebufferseries.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -17,24 +17,74 @@ along with serialplot. If not, see . */ +#include #include "framebufferseries.h" FrameBufferSeries::FrameBufferSeries(FrameBuffer* buffer) { + xAsIndex = true; + _xmin = 0; + _xmax = 1; _buffer = buffer; + int_index_start = 0; + int_index_end = 0; +} + +void FrameBufferSeries::setXAxis(bool asIndex, double xmin, double xmax) +{ + xAsIndex = asIndex; + _xmin = xmin; + _xmax = xmax; } size_t FrameBufferSeries::size() const { - return _buffer->size(); + return int_index_end - int_index_start; } QPointF FrameBufferSeries::sample(size_t i) const { - return QPointF(i, _buffer->sample(i)); + i += int_index_start; + if (xAsIndex) + { + return QPointF(i, _buffer->sample(i)); + } + else + { + return QPointF(i * (_xmax - _xmin) / _buffer->size() + _xmin, _buffer->sample(i)); + } } QRectF FrameBufferSeries::boundingRect() const { - return _buffer->boundingRect(); + if (xAsIndex) + { + return _buffer->boundingRect(); + } + else + { + auto rect = _buffer->boundingRect(); + rect.setLeft(_xmin); + rect.setRight(_xmax); + return rect; + } } + +void FrameBufferSeries::setRectOfInterest(const QRectF& rect) +{ + if (xAsIndex) + { + int_index_start = floor(rect.left())-1; + int_index_end = ceil(rect.right())+1; + } + else + { + double xsize = _xmax - _xmin; + size_t bsize = _buffer->size(); + int_index_start = floor(bsize * (rect.left()-_xmin) / xsize)-1; + int_index_end = ceil(bsize * (rect.right()-_xmin) / xsize)+1; + } + + int_index_start = std::max(int_index_start, (size_t) 0); + int_index_end = std::min(_buffer->size(), int_index_end); +} diff --git a/src/framebufferseries.h b/src/framebufferseries.h --- a/src/framebufferseries.h +++ b/src/framebufferseries.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -37,13 +37,23 @@ class FrameBufferSeries : public QwtSeri public: FrameBufferSeries(FrameBuffer* buffer); + /// Behavior of X axis + void setXAxis(bool asIndex, double xmin, double xmax); + // QwtSeriesData implementations size_t size() const; QPointF sample(size_t i) const; QRectF boundingRect() const; + void setRectOfInterest(const QRectF& rect); private: FrameBuffer* _buffer; + bool xAsIndex; + double _xmin; + double _xmax; + + size_t int_index_start; ///< starting index of "rectangle of interest" + size_t int_index_end; ///< ending index of "rectangle of interest" }; #endif // FRAMEBUFFERSERIES_H diff --git a/src/hidabletabwidget.cpp b/src/hidabletabwidget.cpp --- a/src/hidabletabwidget.cpp +++ b/src/hidabletabwidget.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2015 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -77,3 +77,8 @@ void HidableTabWidget::connectSignals() connect(this, SIGNAL(tabBarDoubleClicked(int)), this, SLOT(onTabBarDoubleClicked())); } } + +void HidableTabWidget::showTabs() +{ + hideAction.setChecked(false); +} diff --git a/src/hidabletabwidget.h b/src/hidabletabwidget.h --- a/src/hidabletabwidget.h +++ b/src/hidabletabwidget.h @@ -1,5 +1,5 @@ /* - Copyright © 2015 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -30,6 +30,9 @@ public: explicit HidableTabWidget(QWidget *parent = 0); QAction hideAction; +public slots: + void showTabs(); + private slots: void onHideAction(bool checked); void onTabBarClicked(); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -63,7 +63,8 @@ MainWindow::MainWindow(QWidget *parent) snapshotMan(this, &channelMan), commandPanel(&serialPort), dataFormatPanel(&serialPort, &channelMan, &recorder), - recordPanel(&recorder, &channelMan) + recordPanel(&recorder, &channelMan), + updateCheckDialog(this) { ui->setupUi(this); @@ -86,6 +87,7 @@ MainWindow::MainWindow(QWidget *parent) connect(&commandPanel, &CommandPanel::focusRequested, [this]() { this->ui->tabWidget->setCurrentWidget(&commandPanel); + this->ui->tabWidget->showTabs(); }); tbPortControl->setObjectName("tbPortControl"); @@ -111,6 +113,9 @@ MainWindow::MainWindow(QWidget *parent) QObject::connect(ui->actionHelpAbout, &QAction::triggered, &aboutDialog, &QWidget::show); + QObject::connect(ui->actionCheckUpdate, &QAction::triggered, + &updateCheckDialog, &QWidget::show); + QObject::connect(ui->actionReportBug, &QAction::triggered, [](){QDesktopServices::openUrl(QUrl(BUG_REPORT_URL));}); @@ -133,14 +138,18 @@ MainWindow::MainWindow(QWidget *parent) QObject::connect(&portControl, &PortControl::portToggled, this, &MainWindow::onPortToggled); + // plot control signals connect(&plotControlPanel, &PlotControlPanel::numOfSamplesChanged, this, &MainWindow::onNumOfSamplesChanged); connect(&plotControlPanel, &PlotControlPanel::numOfSamplesChanged, - plotMan, &PlotManager::onNumOfSamplesChanged); + plotMan, &PlotManager::setNumOfSamples); - connect(&plotControlPanel, &PlotControlPanel::scaleChanged, - plotMan, &PlotManager::setAxis); + connect(&plotControlPanel, &PlotControlPanel::yScaleChanged, + plotMan, &PlotManager::setYAxis); + + connect(&plotControlPanel, &PlotControlPanel::xScaleChanged, + plotMan, &PlotManager::setXAxis); QObject::connect(ui->actionClear, SIGNAL(triggered(bool)), this, SLOT(clearPlot())); @@ -211,9 +220,12 @@ MainWindow::MainWindow(QWidget *parent) plotMan->addCurve(channelMan.channelName(i), channelMan.channelBuffer(i)); } - // init auto scale - plotMan->setAxis(plotControlPanel.autoScale(), - plotControlPanel.yMin(), plotControlPanel.yMax()); + // init scales + plotMan->setYAxis(plotControlPanel.autoScale(), + plotControlPanel.yMin(), plotControlPanel.yMax()); + plotMan->setXAxis(plotControlPanel.xAxisAsIndex(), + plotControlPanel.xMin(), plotControlPanel.xMax()); + plotMan->setNumOfSamples(numOfSamples); // Init sps (sample per second) counter spsLabel.setText("0sps"); @@ -246,15 +258,12 @@ MainWindow::MainWindow(QWidget *parent) connect(commandPanel.newCommandAction(), &QAction::triggered, [this]() { this->ui->tabWidget->setCurrentWidget(&commandPanel); + this->ui->tabWidget->showTabs(); }); } MainWindow::~MainWindow() { - // save settings - QSettings settings("serialplot", "serialplot"); - saveAllSettings(&settings); - if (serialPort.isOpen()) { serialPort.close(); @@ -268,19 +277,54 @@ MainWindow::~MainWindow() void MainWindow::closeEvent(QCloseEvent * event) { + // save snapshots if (!snapshotMan.isAllSaved()) { auto clickedButton = QMessageBox::warning( this, "Closing SerialPlot", "There are un-saved snapshots. If you close you will loose the data.", - QMessageBox::Discard | QMessageBox::Discard, - QMessageBox::Cancel); + QMessageBox::Discard, QMessageBox::Cancel); if (clickedButton == QMessageBox::Cancel) { event->ignore(); return; } } + + // save settings + QSettings settings("serialplot", "serialplot"); + saveAllSettings(&settings); + settings.sync(); + + if (settings.status() != QSettings::NoError) + { + QString errorText; + + if (settings.status() == QSettings::AccessError) + { + QString file = settings.fileName(); + errorText = QString("Serialplot cannot save settings due to access error. \ +This happens if you have run serialplot as root (with sudo for ex.) previously. \ +Try fixing the permissions of file: %1, or just delete it.").arg(file); + } + else + { + errorText = QString("Serialplot cannot save settings due to unknown error: %1").\ + arg(settings.status()); + } + + auto button = QMessageBox::critical( + NULL, + "Failed to save settings!", errorText, + QMessageBox::Cancel | QMessageBox::Ok); + + if (button == QMessageBox::Cancel) + { + event->ignore(); + return; + } + } + QMainWindow::closeEvent(event); } @@ -449,6 +493,11 @@ void MainWindow::onExportCsv() } } +PlotViewSettings MainWindow::viewSettings() const +{ + return plotMan->viewSettings(); +} + void MainWindow::messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) @@ -500,6 +549,7 @@ void MainWindow::saveAllSettings(QSettin plotMan->saveSettings(settings); commandPanel.saveSettings(settings); recordPanel.saveSettings(settings); + updateCheckDialog.saveSettings(settings); } void MainWindow::loadAllSettings(QSettings* settings) @@ -512,6 +562,7 @@ void MainWindow::loadAllSettings(QSettin plotMan->loadSettings(settings); commandPanel.loadSettings(settings); recordPanel.loadSettings(settings); + updateCheckDialog.loadSettings(settings); } void MainWindow::saveMWSettings(QSettings* settings) diff --git a/src/mainwindow.h b/src/mainwindow.h --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -45,6 +45,7 @@ #include "snapshotmanager.h" #include "plotmanager.h" #include "datarecorder.h" +#include "updatecheckdialog.h" namespace Ui { class MainWindow; @@ -58,6 +59,8 @@ public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); + PlotViewSettings viewSettings() const; + void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); @@ -83,6 +86,7 @@ private: DataFormatPanel dataFormatPanel; RecordPanel recordPanel; PlotControlPanel plotControlPanel; + UpdateCheckDialog updateCheckDialog; bool isDemoRunning(); /// Stores settings for all modules diff --git a/src/mainwindow.ui b/src/mainwindow.ui --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -90,7 +90,7 @@ 0 0 653 - 27 + 25 @@ -99,6 +99,7 @@ + @@ -141,6 +142,11 @@ false + + + + + Pause @@ -152,6 +158,11 @@ + + + + + Clear @@ -215,6 +226,11 @@ Load Settings from a File + + + &Check Update + + diff --git a/src/plot.cpp b/src/plot.cpp --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2015 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -38,6 +38,8 @@ Plot::Plot(QWidget* parent) : { isAutoScaled = true; symbolSize = 0; + numOfSamples = 1; + showSymbols = Plot::ShowSymbolsAuto; QObject::connect(&zoomer, &Zoomer::unzoomed, this, &Plot::unzoomed); @@ -71,6 +73,16 @@ Plot::Plot(QWidget* parent) : demoIndicator.setText(demoText); demoIndicator.hide(); demoIndicator.attach(this); + + // init no channels are visible indicator + QwtText noChannelText(" No Visible Channels "); + noChannelText.setColor(QColor("white")); + noChannelText.setBackgroundBrush(Qt::darkBlue); + noChannelText.setBorderRadius(4); + noChannelText.setRenderFlags(Qt::AlignHCenter | Qt::AlignVCenter); + noChannelIndicator.setText(noChannelText); + noChannelIndicator.hide(); + noChannelIndicator.attach(this); } Plot::~Plot() @@ -78,7 +90,7 @@ Plot::~Plot() if (snapshotOverlay != NULL) delete snapshotOverlay; } -void Plot::setAxis(bool autoScaled, double yAxisMin, double yAxisMax) +void Plot::setYAxis(bool autoScaled, double yAxisMin, double yAxisMax) { this->isAutoScaled = autoScaled; @@ -92,8 +104,29 @@ void Plot::setAxis(bool autoScaled, doub resetAxes(); } +void Plot::setXAxis(double xMin, double xMax) +{ + _xMin = xMin; + _xMax = xMax; + + zoomer.zoom(0); // unzoom + + // set axis + setAxisScale(QwtPlot::xBottom, xMin, xMax); + replot(); // Note: if we don't replot here scale at startup isn't set correctly + + // reset zoom base + auto base = zoomer.zoomBase(); + base.setLeft(xMin); + base.setRight(xMax); + zoomer.setZoomBase(base); + + onXScaleChanged(); +} + void Plot::resetAxes() { + // reset y axis if (isAutoScaled) { setAxisAutoScale(QwtPlot::yLeft); @@ -103,12 +136,13 @@ void Plot::resetAxes() setAxisScale(QwtPlot::yLeft, yMin, yMax); } + zoomer.setZoomBase(); + replot(); } void Plot::unzoomed() { - setAxisAutoScale(QwtPlot::xBottom); resetAxes(); onXScaleChanged(); } @@ -139,6 +173,12 @@ void Plot::showDemoIndicator(bool show) replot(); } +void Plot::showNoChannel(bool show) +{ + noChannelIndicator.setVisible(show); + replot(); +} + void Plot::unzoom() { zoomer.zoom(0); @@ -224,12 +264,45 @@ void Plot::flashSnapshotOverlay(bool lig }); } +void Plot::setSymbols(ShowSymbols shown) +{ + showSymbols = shown; + + if (showSymbols == Plot::ShowSymbolsAuto) + { + calcSymbolSize(); + } + else if (showSymbols == Plot::ShowSymbolsShow) + { + symbolSize = SYMBOL_SIZE_MAX; + } + else + { + symbolSize = 0; + } + + updateSymbols(); + replot(); +} + void Plot::onXScaleChanged() { + if (showSymbols == Plot::ShowSymbolsAuto) + { + calcSymbolSize(); + updateSymbols(); + } +} + +void Plot::calcSymbolSize() +{ auto sw = axisWidget(QwtPlot::xBottom); auto paintDist = sw->scaleDraw()->scaleMap().pDist(); auto scaleDist = sw->scaleDraw()->scaleMap().sDist(); - int symDisPx = round(paintDist / scaleDist); + auto fullScaleDist = zoomer.zoomBase().width(); + auto zoomRate = fullScaleDist / scaleDist; + float samplesInView = numOfSamples / zoomRate; + int symDisPx = round(paintDist / samplesInView); if (symDisPx < SYMBOL_SHOW_AT_WIDTH) { @@ -239,8 +312,6 @@ void Plot::onXScaleChanged() { symbolSize = std::min(SYMBOL_SIZE_MAX, symDisPx-SYMBOL_SHOW_AT_WIDTH+1); } - - updateSymbols(); } void Plot::updateSymbols() @@ -271,10 +342,8 @@ void Plot::resizeEvent(QResizeEvent * ev onXScaleChanged(); } -void Plot::onNumOfSamplesChanged(unsigned value) +void Plot::setNumOfSamples(unsigned value) { - auto currentBase = zoomer.zoomBase(); - currentBase.setWidth(value); - zoomer.setZoomBase(currentBase); + numOfSamples = value; onXScaleChanged(); } diff --git a/src/plot.h b/src/plot.h --- a/src/plot.h +++ b/src/plot.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -40,6 +40,13 @@ class Plot : public QwtPlot friend class PlotManager; public: + enum ShowSymbols + { + ShowSymbolsAuto, + ShowSymbolsShow, + ShowSymbolsHide + }; + Plot(QWidget* parent = 0); ~Plot(); @@ -50,9 +57,12 @@ public slots: void showMinorGrid(bool show = true); void showLegend(bool show = true); void showDemoIndicator(bool show = true); + void showNoChannel(bool show = true); void unzoom(); void darkBackground(bool enabled = true); - void setAxis(bool autoScaled, double yMin = 0, double yMax = 1); + void setYAxis(bool autoScaled, double yMin = 0, double yMax = 1); + void setXAxis(double xMin, double xMax); + void setSymbols(ShowSymbols shown); /** * Displays an animation for snapshot. @@ -61,7 +71,7 @@ public slots: */ void flashSnapshotOverlay(bool light); - void onNumOfSamplesChanged(unsigned value); + void setNumOfSamples(unsigned value); protected: /// update the display of symbols depending on `symbolSize` @@ -70,6 +80,8 @@ protected: private: bool isAutoScaled; double yMin, yMax; + double _xMin, _xMax; + unsigned numOfSamples; int symbolSize; Zoomer zoomer; ScaleZoomer sZoomer; @@ -77,9 +89,12 @@ private: PlotSnapshotOverlay* snapshotOverlay; QwtPlotLegendItem legend; QwtPlotTextLabel demoIndicator; + QwtPlotTextLabel noChannelIndicator; + ShowSymbols showSymbols; void resetAxes(); void resizeEvent(QResizeEvent * event); + void calcSymbolSize(); private slots: void unzoomed(); diff --git a/src/plotcontrolpanel.cpp b/src/plotcontrolpanel.cpp --- a/src/plotcontrolpanel.cpp +++ b/src/plotcontrolpanel.cpp @@ -61,6 +61,12 @@ PlotControlPanel::PlotControlPanel(QWidg ui->spYmax->setRange((-1) * std::numeric_limits::max(), std::numeric_limits::max()); + ui->spXmin->setRange((-1) * std::numeric_limits::max(), + std::numeric_limits::max()); + + ui->spXmax->setRange((-1) * std::numeric_limits::max(), + std::numeric_limits::max()); + // connect signals connect(ui->spNumOfSamples, SIGNAL(valueChanged(int)), this, SLOT(onNumOfSamples(int))); @@ -74,6 +80,15 @@ PlotControlPanel::PlotControlPanel(QWidg connect(ui->spYmin, SIGNAL(valueChanged(double)), this, SLOT(onYScaleChanged())); + connect(ui->cbIndex, &QCheckBox::toggled, + this, &PlotControlPanel::onIndexChecked); + + connect(ui->spXmax, SIGNAL(valueChanged(double)), + this, SLOT(onXScaleChanged())); + + connect(ui->spXmin, SIGNAL(valueChanged(double)), + this, SLOT(onXScaleChanged())); + // init scale range preset list for (int nbits = 8; nbits <= 24; nbits++) // signed binary formats { @@ -181,7 +196,7 @@ void PlotControlPanel::onAutoScaleChecke ui->spYmin->setEnabled(false); ui->spYmax->setEnabled(false); - emit scaleChanged(true); // autoscale + emit yScaleChanged(true); // autoscale } else { @@ -190,30 +205,48 @@ void PlotControlPanel::onAutoScaleChecke ui->spYmin->setEnabled(true); ui->spYmax->setEnabled(true); - emit scaleChanged(false, ui->spYmin->value(), ui->spYmax->value()); + emit yScaleChanged(false, ui->spYmin->value(), ui->spYmax->value()); } } void PlotControlPanel::onYScaleChanged() { - emit scaleChanged(false, ui->spYmin->value(), ui->spYmax->value()); + if (!autoScale()) + { + emit yScaleChanged(false, ui->spYmin->value(), ui->spYmax->value()); + } } -bool PlotControlPanel::autoScale() +bool PlotControlPanel::autoScale() const { return ui->cbAutoScale->isChecked(); } -double PlotControlPanel::yMax() +double PlotControlPanel::yMax() const { return ui->spYmax->value(); } -double PlotControlPanel::yMin() +double PlotControlPanel::yMin() const { return ui->spYmin->value(); } +bool PlotControlPanel::xAxisAsIndex() const +{ + return ui->cbIndex->isChecked(); +} + +double PlotControlPanel::xMax() const +{ + return ui->spXmax->value(); +} + +double PlotControlPanel::xMin() const +{ + return ui->spXmin->value(); +} + void PlotControlPanel::onRangeSelected() { Range r = ui->cbRangePresets->currentData().value(); @@ -222,6 +255,36 @@ void PlotControlPanel::onRangeSelected() ui->cbAutoScale->setChecked(false); } +void PlotControlPanel::onIndexChecked(bool checked) +{ + if (checked) + { + ui->lXmin->setEnabled(false); + ui->lXmax->setEnabled(false); + ui->spXmin->setEnabled(false); + ui->spXmax->setEnabled(false); + + emit xScaleChanged(true); // use index + } + else + { + ui->lXmin->setEnabled(true); + ui->lXmax->setEnabled(true); + ui->spXmin->setEnabled(true); + ui->spXmax->setEnabled(true); + + emit xScaleChanged(false, ui->spXmin->value(), ui->spXmax->value()); + } +} + +void PlotControlPanel::onXScaleChanged() +{ + if (!xAxisAsIndex()) + { + emit xScaleChanged(false, ui->spXmin->value(), ui->spXmax->value()); + } +} + void PlotControlPanel::setChannelInfoModel(ChannelInfoModel* model) { ui->tvChannelInfo->setModel(model); @@ -299,6 +362,9 @@ void PlotControlPanel::saveSettings(QSet { settings->beginGroup(SettingGroup_Plot); settings->setValue(SG_Plot_NumOfSamples, numOfSamples()); + settings->setValue(SG_Plot_IndexAsX, xAxisAsIndex()); + settings->setValue(SG_Plot_XMax, xMax()); + settings->setValue(SG_Plot_XMin, xMin()); settings->setValue(SG_Plot_AutoScale, autoScale()); settings->setValue(SG_Plot_YMax, yMax()); settings->setValue(SG_Plot_YMin, yMin()); @@ -310,6 +376,10 @@ void PlotControlPanel::loadSettings(QSet settings->beginGroup(SettingGroup_Plot); ui->spNumOfSamples->setValue( settings->value(SG_Plot_NumOfSamples, numOfSamples()).toInt()); + ui->cbIndex->setChecked( + settings->value(SG_Plot_IndexAsX, xAxisAsIndex()).toBool()); + ui->spXmax->setValue(settings->value(SG_Plot_XMax, xMax()).toDouble()); + ui->spXmin->setValue(settings->value(SG_Plot_XMin, xMin()).toDouble()); ui->cbAutoScale->setChecked( settings->value(SG_Plot_AutoScale, autoScale()).toBool()); ui->spYmax->setValue(settings->value(SG_Plot_YMax, yMax()).toDouble()); diff --git a/src/plotcontrolpanel.h b/src/plotcontrolpanel.h --- a/src/plotcontrolpanel.h +++ b/src/plotcontrolpanel.h @@ -40,9 +40,12 @@ public: ~PlotControlPanel(); unsigned numOfSamples(); - bool autoScale(); - double yMax(); - double yMin(); + bool autoScale() const; + double yMax() const; + double yMin() const; + bool xAxisAsIndex() const; + double xMax() const; + double xMin() const; void setChannelInfoModel(ChannelInfoModel* model); @@ -53,7 +56,8 @@ public: signals: void numOfSamplesChanged(int value); - void scaleChanged(bool autoScaled, double yMin = 0, double yMax = 1); + void yScaleChanged(bool autoScaled, double yMin = 0, double yMax = 1); + void xScaleChanged(bool asIndex, double xMin = 0, double xMax = 1); private: Ui::PlotControlPanel *ui; @@ -74,6 +78,8 @@ private slots: void onAutoScaleChecked(bool checked); void onYScaleChanged(); void onRangeSelected(); + void onIndexChecked(bool checked); + void onXScaleChanged(); }; #endif // PLOTCONTROLPANEL_H diff --git a/src/plotcontrolpanel.ui b/src/plotcontrolpanel.ui --- a/src/plotcontrolpanel.ui +++ b/src/plotcontrolpanel.ui @@ -6,7 +6,7 @@ 0 0 - 590 + 706 187 @@ -157,6 +157,87 @@ + + + Index as X AXis + + + true + + + + + + + + + false + + + Xmin + + + + + + + false + + + + 75 + 0 + + + + + 75 + 16777215 + + + + lower limit of Y axis + + + 0.000000000000000 + + + + + + + false + + + Xmax + + + + + + + false + + + + 75 + 16777215 + + + + upper limit of Y axis + + + 1000.000000000000000 + + + 1000.000000000000000 + + + + + + Auto Scale Y Axis @@ -166,75 +247,85 @@ - - - - false - - - Ymax - - + + + + + + false + + + Ymin + + + + + + + false + + + + 75 + 0 + + + + + 75 + 16777215 + + + + lower limit of Y axis + + + 0.000000000000000 + + + + + + + false + + + Ymax + + + + + + + false + + + + 75 + 16777215 + + + + upper limit of Y axis + + + 1000.000000000000000 + + + 1000.000000000000000 + + + + - - - - false - - - - 75 - 16777215 - - - - upper limit of Y axis - - - 1000.000000000000000 - - - 1000.000000000000000 - - - - - - - false - - - Ymin - - - - - - - false - - - - 75 - 16777215 - - - - lower limit of Y axis - - - 0.000000000000000 - - - - + Select Range Preset: - + diff --git a/src/plotmanager.cpp b/src/plotmanager.cpp --- a/src/plotmanager.cpp +++ b/src/plotmanager.cpp @@ -17,6 +17,9 @@ along with serialplot. If not, see . */ +#include +#include +#include #include #include "qwt_symbol.h" @@ -33,13 +36,18 @@ PlotManager::PlotManager(QWidget* plotAr unzoomAction("&Unzoom", this), darkBackgroundAction("&Dark Background", this), showLegendAction("&Legend", this), - showMultiAction("Multi &Plot", this) + showMultiAction("Multi &Plot", this), + setSymbolsAction("Symbols", this) { _autoScaled = true; _yMin = 0; _yMax = 1; + _xAxisAsIndex = true; isDemoShown = false; _infoModel = infoModel; + _numOfSamples = 1; + showSymbols = Plot::ShowSymbolsAuto; + emptyPlot = NULL; // initalize layout and single widget isMulti = false; @@ -54,6 +62,7 @@ PlotManager::PlotManager(QWidget* plotAr darkBackgroundAction.setToolTip("Enable Dark Plot Background"); showLegendAction.setToolTip("Display the Legend on Plot"); showMultiAction.setToolTip("Display All Channels Separately"); + setSymbolsAction.setToolTip("Show/Hide symbols"); showGridAction.setShortcut(QKeySequence("G")); showMinorGridAction.setShortcut(QKeySequence("M")); @@ -72,6 +81,37 @@ PlotManager::PlotManager(QWidget* plotAr showMinorGridAction.setEnabled(false); + // setup symbols menu + setSymbolsAutoAct = setSymbolsMenu.addAction("Show When Zoomed"); + setSymbolsAutoAct->setCheckable(true); + setSymbolsAutoAct->setChecked(true); + connect(setSymbolsAutoAct, SELECT::OVERLOAD_OF(&QAction::triggered), + [this](bool checked) + { + if (checked) setSymbols(Plot::ShowSymbolsAuto); + }); + setSymbolsShowAct = setSymbolsMenu.addAction("Always Show"); + setSymbolsShowAct->setCheckable(true); + connect(setSymbolsShowAct, SELECT::OVERLOAD_OF(&QAction::triggered), + [this](bool checked) + { + if (checked) setSymbols(Plot::ShowSymbolsShow); + }); + setSymbolsHideAct = setSymbolsMenu.addAction("Always Hide"); + setSymbolsHideAct->setCheckable(true); + connect(setSymbolsHideAct, SELECT::OVERLOAD_OF(&QAction::triggered), + [this](bool checked) + { + if (checked) setSymbols(Plot::ShowSymbolsHide); + }); + setSymbolsAction.setMenu(&setSymbolsMenu); + + // add symbol actions to same group so that they appear as radio buttons + auto group = new QActionGroup(this); + group->addAction(setSymbolsAutoAct); + group->addAction(setSymbolsShowAct); + group->addAction(setSymbolsHideAct); + connect(&showGridAction, SELECT::OVERLOAD_OF(&QAction::triggered), this, &PlotManager::showGrid); connect(&showGridAction, SELECT::OVERLOAD_OF(&QAction::triggered), @@ -118,6 +158,7 @@ PlotManager::~PlotManager() } if (scrollArea != NULL) delete scrollArea; + if (emptyPlot != NULL) delete emptyPlot; } void PlotManager::onChannelInfoChanged(const QModelIndex &topLeft, @@ -151,6 +192,8 @@ void PlotManager::onChannelInfoChanged(c } } + checkNoVisChannels(); + // replot single widget if (!isMulti) { @@ -160,6 +203,20 @@ void PlotManager::onChannelInfoChanged(c } } +void PlotManager::checkNoVisChannels() +{ + // if all channels are hidden show indicator + bool allhidden = std::none_of(curves.cbegin(), curves.cend(), + [](QwtPlotCurve* c) {return c->isVisible();}); + + plotWidgets[0]->showNoChannel(allhidden); + if (isMulti) + { + plotWidgets[0]->showNoChannel(allhidden); + plotWidgets[0]->setVisible(true); + } +} + void PlotManager::setMulti(bool enabled) { if (enabled == isMulti) return; @@ -186,7 +243,9 @@ void PlotManager::setMulti(bool enabled) // add new widgets and attach for (auto curve : curves) { - curve->attach(addPlotWidget()); + auto plot = addPlotWidget(); + plot->setVisible(curve->isVisible()); + curve->attach(plot); } } else @@ -200,6 +259,8 @@ void PlotManager::setMulti(bool enabled) curve->attach(plot); } } + + checkNoVisChannels(); } void PlotManager::setupLayout(bool multiPlot) @@ -251,7 +312,18 @@ Plot* PlotManager::addPlotWidget() plot->showMinorGrid(showMinorGridAction.isChecked()); plot->showLegend(showLegendAction.isChecked()); plot->showDemoIndicator(isDemoShown); - plot->setAxis(_autoScaled, _yMin, _yMax); + plot->setYAxis(_autoScaled, _yMin, _yMax); + plot->setNumOfSamples(_numOfSamples); + plot->setSymbols(showSymbols); + + if (_xAxisAsIndex) + { + plot->setXAxis(0, _numOfSamples); + } + else + { + plot->setXAxis(_xMin, _xMax); + } return plot; } @@ -259,7 +331,9 @@ Plot* PlotManager::addPlotWidget() void PlotManager::addCurve(QString title, FrameBuffer* buffer) { auto curve = new QwtPlotCurve(title); - curve->setSamples(new FrameBufferSeries(buffer)); + auto series = new FrameBufferSeries(buffer); + series->setXAxis(_xAxisAsIndex, _xMin, _xMax); + curve->setSamples(series); _addCurve(curve); } @@ -352,6 +426,7 @@ QList PlotManager::menuActions actions << &darkBackgroundAction; actions << &showLegendAction; actions << &showMultiAction; + actions << &setSymbolsAction; return actions; } @@ -404,17 +479,51 @@ void PlotManager::darkBackground(bool en } } -void PlotManager::setAxis(bool autoScaled, double yAxisMin, double yAxisMax) +void PlotManager::setSymbols(Plot::ShowSymbols shown) +{ + showSymbols = shown; + for (auto plot : plotWidgets) + { + plot->setSymbols(shown); + } +} + +void PlotManager::setYAxis(bool autoScaled, double yAxisMin, double yAxisMax) { _autoScaled = autoScaled; _yMin = yAxisMin; _yMax = yAxisMax; for (auto plot : plotWidgets) { - plot->setAxis(autoScaled, yAxisMin, yAxisMax); + plot->setYAxis(autoScaled, yAxisMin, yAxisMax); } } +void PlotManager::setXAxis(bool asIndex, double xMin, double xMax) +{ + _xAxisAsIndex = asIndex; + _xMin = xMin; + _xMax = xMax; + for (auto curve : curves) + { + // TODO: what happens when addCurve(QVector) is used? + FrameBufferSeries* series = static_cast(curve->data()); + series->setXAxis(asIndex, xMin, xMax); + } + for (auto plot : plotWidgets) + { + if (asIndex) + { + plot->setXAxis(0, _numOfSamples); + } + else + { + plot->setXAxis(xMin, xMax); + } + } + replot(); +} + void PlotManager::flashSnapshotOverlay() { for (auto plot : plotWidgets) @@ -423,11 +532,54 @@ void PlotManager::flashSnapshotOverlay() } } -void PlotManager::onNumOfSamplesChanged(unsigned value) +void PlotManager::setNumOfSamples(unsigned value) { + _numOfSamples = value; for (auto plot : plotWidgets) { - plot->onNumOfSamplesChanged(value); + plot->setNumOfSamples(value); + if (_xAxisAsIndex) plot->setXAxis(0, value); + } +} + +PlotViewSettings PlotManager::viewSettings() const +{ + return PlotViewSettings( + { + showGridAction.isChecked(), + showMinorGridAction.isChecked(), + darkBackgroundAction.isChecked(), + showLegendAction.isChecked(), + showMultiAction.isChecked(), + showSymbols + }); +} + +void PlotManager::setViewSettings(const PlotViewSettings& settings) +{ + showGridAction.setChecked(settings.showGrid); + showGrid(settings.showGrid); + showMinorGridAction.setChecked(settings.showMinorGrid); + showMinorGrid(settings.showMinorGrid); + darkBackgroundAction.setChecked(settings.darkBackground); + darkBackground(settings.darkBackground); + showLegendAction.setChecked(settings.showLegend); + showLegend(settings.showLegend); + showMultiAction.setChecked(settings.showMulti); + setMulti(settings.showMulti); + + setSymbols(settings.showSymbols); + if (showSymbols == Plot::ShowSymbolsAuto) + { + setSymbolsAutoAct->setChecked(true); + } + else if (showSymbols == Plot::ShowSymbolsShow) + { + setSymbolsShowAct->setChecked(true); + } + else + { + setSymbolsHideAct->setChecked(true); } } @@ -439,6 +591,22 @@ void PlotManager::saveSettings(QSettings settings->setValue(SG_Plot_MinorGrid, showMinorGridAction.isChecked()); settings->setValue(SG_Plot_Legend, showLegendAction.isChecked()); settings->setValue(SG_Plot_MultiPlot, showMultiAction.isChecked()); + + QString showSymbolsStr; + if (showSymbols == Plot::ShowSymbolsAuto) + { + showSymbolsStr = "auto"; + } + else if (showSymbols == Plot::ShowSymbolsShow) + { + showSymbolsStr = "show"; + } + else + { + showSymbolsStr = "hide"; + } + settings->setValue(SG_Plot_Symbols, showSymbolsStr); + settings->endGroup(); } @@ -461,5 +629,27 @@ void PlotManager::loadSettings(QSettings showMultiAction.setChecked( settings->value(SG_Plot_MultiPlot, showMultiAction.isChecked()).toBool()); setMulti(showMultiAction.isChecked()); + + QString showSymbolsStr = settings->value(SG_Plot_Symbols, QString()).toString(); + if (showSymbolsStr == "auto") + { + setSymbols(Plot::ShowSymbolsAuto); + setSymbolsAutoAct->setChecked(true); + } + else if (showSymbolsStr == "show") + { + setSymbols(Plot::ShowSymbolsShow); + setSymbolsShowAct->setChecked(true); + } + else if (showSymbolsStr == "hide") + { + setSymbols(Plot::ShowSymbolsHide); + setSymbolsHideAct->setChecked(true); + } + else + { + qCritical() << "Invalid symbol setting:" << showSymbolsStr; + } + settings->endGroup(); } diff --git a/src/plotmanager.h b/src/plotmanager.h --- a/src/plotmanager.h +++ b/src/plotmanager.h @@ -26,12 +26,24 @@ #include #include #include +#include +#include #include #include "plot.h" #include "framebufferseries.h" #include "channelinfomodel.h" +struct PlotViewSettings +{ + bool showGrid; + bool showMinorGrid; + bool darkBackground; + bool showLegend; + bool showMulti; + Plot::ShowSymbols showSymbols; +}; + class PlotManager : public QObject { Q_OBJECT @@ -52,6 +64,10 @@ public: unsigned numOfCurves(); /// Returns the list of actions to be inserted into the `View` menu QList menuActions(); + /// Returns current status of menu actions + PlotViewSettings viewSettings() const; + /// Set the current state of view + void setViewSettings(const PlotViewSettings& settings); /// Stores plot settings into a `QSettings`. void saveSettings(QSettings* settings); /// Loads plot settings from a `QSettings`. @@ -65,11 +81,13 @@ public slots: /// Enable display of a "DEMO" label on each plot void showDemoIndicator(bool show = true); /// Set the Y axis - void setAxis(bool autoScaled, double yMin = 0, double yMax = 1); + void setYAxis(bool autoScaled, double yMin = 0, double yMax = 1); + /// Set the X axis + void setXAxis(bool asIndex, double xMin = 0 , double xMax = 1); /// Display an animation for snapshot void flashSnapshotOverlay(); /// Should be called to update zoom base - void onNumOfSamplesChanged(unsigned value); + void setNumOfSamples(unsigned value); private: bool isMulti; @@ -78,11 +96,17 @@ private: QScrollArea* scrollArea; QList curves; QList plotWidgets; + Plot* emptyPlot; ///< for displaying when all channels are hidden ChannelInfoModel* _infoModel; bool isDemoShown; bool _autoScaled; double _yMin; double _yMax; + bool _xAxisAsIndex; + double _xMin; + double _xMax; + unsigned _numOfSamples; + Plot::ShowSymbols showSymbols; // menu actions QAction showGridAction; @@ -91,6 +115,11 @@ private: QAction darkBackgroundAction; QAction showLegendAction; QAction showMultiAction; + QAction setSymbolsAction; + QMenu setSymbolsMenu; + QAction* setSymbolsAutoAct; + QAction* setSymbolsShowAct; + QAction* setSymbolsHideAct; void setupLayout(bool multiPlot); /// Inserts a new plot widget to the current layout. @@ -99,6 +128,9 @@ private: Plot* plotWidget(unsigned curveIndex); /// Common part of overloaded `addCurve` functions void _addCurve(QwtPlotCurve* curve); + void setSymbols(Plot::ShowSymbols shown); + /// Check and make sure "no visible channels" text is shown + void checkNoVisChannels(); private slots: void showGrid(bool show = true); diff --git a/src/recordpanel.cpp b/src/recordpanel.cpp --- a/src/recordpanel.cpp +++ b/src/recordpanel.cpp @@ -60,6 +60,14 @@ RecordPanel::RecordPanel(DataRecorder* r { _recorder->disableBuffering = enabled; }); + + connect(ui->cbWindowsLE, &QCheckBox::toggled, + [this](bool enabled) + { + _recorder->windowsLE = enabled; + }); + + connect(&recordAction, &QAction::toggled, ui->cbWindowsLE, &QWidget::setDisabled); } RecordPanel::~RecordPanel() diff --git a/src/recordpanel.ui b/src/recordpanel.ui --- a/src/recordpanel.ui +++ b/src/recordpanel.ui @@ -6,7 +6,7 @@ 0 0 - 532 + 627 261 @@ -44,53 +44,93 @@ - - - Increments file name automatically everytime a new recording starts - - - Auto increment file name - - - true - - - - - - - Continue recording to file even when plotting is paused - - - Record while paused - - - true - - - - - - - Stop recording when port closed - - - true - - - - - - - Channel names are written to the first line of record file - - - Write header line - - - true - - + + + + + Channel names are written to the first line of record file + + + Write header line + + + true + + + + + + + Continue recording to file even when plotting is paused + + + Record while paused + + + true + + + + + + + Do not buffer when writing to file. Check this if you are using other software to open the file during recording. + + + Disable buffering + + + + + + + Stop recording when port closed + + + true + + + + + + + Increments file name automatically everytime a new recording starts + + + Auto increment file name + + + true + + + + + + + Use CR+LF as line endings. Some windows software may not show lines correctly otherwise. Can't be changed during recording. + + + Windows Style Line Endings + + + false + + + + + + + Qt::Horizontal + + + + 1 + 20 + + + + + @@ -133,16 +173,6 @@ - - - Do not buffer when writing to file. Check this if you are using other software to open the file during recording. - - - Disable buffering - - - - Qt::Vertical diff --git a/src/scalepicker.cpp b/src/scalepicker.cpp --- a/src/scalepicker.cpp +++ b/src/scalepicker.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2015 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -23,6 +23,7 @@ #include #include #include +#include #include #include "scalepicker.h" @@ -107,7 +108,7 @@ bool ScalePicker::eventFilter(QObject* o { for (auto sp : snapPoints) { - if (fabs(posPx-sp) <= SNAP_DISTANCE) + if (std::abs(posPx-sp) <= SNAP_DISTANCE) { posPx = sp; break; @@ -132,13 +133,8 @@ bool ScalePicker::eventFilter(QObject* o if (!started && pressed && (fabs(posPx-firstPosPx) > MIN_PICK_SIZE)) { started = true; - emit pickStarted(pos); } - else if (started) - { - pickerOverlay->updateOverlay(); - emit picking(firstPos, pos); - } + pickerOverlay->updateOverlay(); scaleOverlay->updateOverlay(); } else // event->type() == QEvent::MouseButtonRelease @@ -148,14 +144,20 @@ bool ScalePicker::eventFilter(QObject* o { // finalize started = false; - emit picked(firstPos, pos); + if (firstPos != pos) // ignore 0 width zoom + { + emit picked(firstPos, pos); + } } + pickerOverlay->updateOverlay(); + scaleOverlay->updateOverlay(); } return true; } else if (event->type() == QEvent::Leave) { scaleOverlay->updateOverlay(); + pickerOverlay->updateOverlay(); return true; } else @@ -164,54 +166,222 @@ bool ScalePicker::eventFilter(QObject* o } } +const int TEXT_MARGIN = 4; + void ScalePicker::drawPlotOverlay(QPainter* painter) { + const double FILL_ALPHA = 0.2; + + painter->save(); + painter->setPen(_pen); + if (started) { - painter->save(); - painter->setPen(_pen); + QColor color = _pen.color(); + color.setAlphaF(FILL_ALPHA); + painter->setBrush(color); QRect rect; + QwtText text = trackerText(); + auto tSize = text.textSize(painter->font()); + if (_scaleWidget->alignment() == QwtScaleDraw::BottomScale || _scaleWidget->alignment() == QwtScaleDraw::TopScale) { - int height = painter->device()->height(); - rect = QRect(posCanvasPx(firstPosPx), 0, currentPosPx-firstPosPx, height); + int canvasHeight = painter->device()->height(); + int pickWidth = currentPosPx-firstPosPx; + rect = QRect(posCanvasPx(firstPosPx), 0, pickWidth, canvasHeight); } else // vertical { - int width = painter->device()->width(); - rect = QRect(0, posCanvasPx(firstPosPx), width, currentPosPx-firstPosPx); + int canvasWidth = painter->device()->width(); + int pickHeight = currentPosPx-firstPosPx; + rect = QRect(0, posCanvasPx(firstPosPx), canvasWidth, pickHeight); } painter->drawRect(rect); - painter->restore(); + text.draw(painter, pickTrackerTextRect(painter, rect, tSize)); + } + else if (_scaleWidget->underMouse()) + { + // draw tracker text centered on cursor + QwtText text = trackerText(); + auto tsize = text.textSize(painter->font()); + text.draw(painter, trackerTextRect(painter, currentPosPx, tsize)); + } + painter->restore(); +} + +QwtText ScalePicker::trackerText() const +{ + double pos; + // use stored value if snapped to restore precision + if (snapPointMap.contains(currentPosPx)) + { + pos = snapPointMap[currentPosPx]; + } + else + { + pos = position(currentPosPx); + } + + return QwtText(QString("%1").arg(pos)); +} + +QRectF ScalePicker::trackerTextRect(QPainter* painter, int posPx, QSizeF textSize) const +{ + int canvasPosPx = posCanvasPx(posPx); + QPointF topLeft; + + if (_scaleWidget->alignment() == QwtScaleDraw::BottomScale || + _scaleWidget->alignment() == QwtScaleDraw::TopScale) + { + int left = canvasPosPx - textSize.width() / 2; + int canvasWidth = painter->device()->width(); + left = std::max(TEXT_MARGIN, left); + left = std::min(double(left), canvasWidth - textSize.width() - TEXT_MARGIN); + int top = 0; + if (_scaleWidget->alignment() == QwtScaleDraw::BottomScale) + { + top = painter->device()->height() - textSize.height(); + } + topLeft = QPointF(left, top); + } + else // left/right scales + { + int top = canvasPosPx-textSize.height() / 2; + int canvasHeight = painter->device()->height(); + top = std::max(0, top); + top = std::min(double(top), canvasHeight - textSize.height()); + int left = TEXT_MARGIN; + if (_scaleWidget->alignment() == QwtScaleDraw::RightScale) + { + left = painter->device()->width() - textSize.width(); + } + topLeft = QPointF(left, top); } + return QRectF(topLeft, textSize); +} + +QRectF ScalePicker::pickTrackerTextRect(QPainter* painter, QRect pickRect, QSizeF textSize) const +{ + qreal left = 0; + int pickLength = currentPosPx - firstPosPx; + QPointF topLeft; + + if (_scaleWidget->alignment() == QwtScaleDraw::BottomScale || + _scaleWidget->alignment() == QwtScaleDraw::TopScale) + { + int canvasWidth = painter->device()->width(); + + if (pickLength > 0) + { + left = pickRect.right() + TEXT_MARGIN; + } + else + { + left = pickRect.right() - (textSize.width() + TEXT_MARGIN); + } + + // make sure text is not off the canvas + if (left < TEXT_MARGIN) + { + left = std::max(0, pickRect.right()) + TEXT_MARGIN; + } + else if (left + textSize.width() + TEXT_MARGIN > canvasWidth) + { + left = std::min(pickRect.right(), canvasWidth) - (textSize.width() + TEXT_MARGIN); + } + + if (_scaleWidget->alignment() == QwtScaleDraw::BottomScale) + { + int canvasHeight = painter->device()->height(); + topLeft = QPointF(left, canvasHeight - textSize.height()); + } + else // top scale + { + topLeft = QPointF(left, 0); + } + } + else // left/right scale + { + int canvasHeight = painter->device()->height(); + + int top = 0; + if (pickLength > 0) + { + top = pickRect.bottom(); + } + else + { + top = pickRect.bottom() - textSize.height(); + } + + // make sure text is not off the canvas + if (top < 0) + { + top = std::max(0, pickRect.bottom()); + } + else if (top + textSize.height() > canvasHeight) + { + top = std::min(canvasHeight, pickRect.bottom()) - textSize.height(); + } + + if (_scaleWidget->alignment() == QwtScaleDraw::LeftScale) + { + topLeft = QPointF(TEXT_MARGIN, top); + } + else // right scale + { + int canvasWidth = painter->device()->width(); + topLeft = QPointF(canvasWidth - textSize.width() - TEXT_MARGIN, top); + } + } + return QRectF(topLeft, textSize); } void ScalePicker::drawScaleOverlay(QPainter* painter) { painter->save(); - painter->setPen(_pen); - if (_scaleWidget->alignment() == QwtScaleDraw::BottomScale || - _scaleWidget->alignment() == QwtScaleDraw::TopScale) - { - int height = painter->device()->height(); - if (started) painter->drawLine(firstPosPx, 0, firstPosPx, height); - if (started || _scaleWidget->underMouse()) - { - painter->drawLine(currentPosPx, 0, currentPosPx, height); - } - } - else // vertical + // rotate & adjust coordinate system for vertical drawing + if (_scaleWidget->alignment() == QwtScaleDraw::LeftScale || + _scaleWidget->alignment() == QwtScaleDraw::RightScale) // vertical { int width = painter->device()->width(); - if (started) painter->drawLine(0, firstPosPx, width, firstPosPx); - if (started || _scaleWidget->underMouse()) + painter->rotate(90); + painter->translate(0, -width); + } + + // draw the indicators + if (started) drawTriangle(painter, firstPosPx); + if (started || _scaleWidget->underMouse()) + { + drawTriangle(painter, currentPosPx); + } + + painter->restore(); +} + +void ScalePicker::drawTriangle(QPainter* painter, int position) +{ + const double tan60 = 1.732; + const double trsize = 10; + const int TRIANGLE_NUM_POINTS = 3; + const int MARGIN = 2; + const QPointF points[TRIANGLE_NUM_POINTS] = { - painter->drawLine(0, currentPosPx, width, currentPosPx); - } - } + {0, 0}, + {-trsize/tan60 , trsize}, + {trsize/tan60 , trsize} + }; + + painter->save(); + painter->setPen(Qt::NoPen); + painter->setBrush(_scaleWidget->palette().windowText()); + painter->setRenderHint(QPainter::Antialiasing); + + painter->translate(position, MARGIN); + painter->drawPolygon(points, TRIANGLE_NUM_POINTS); painter->restore(); } @@ -222,7 +392,7 @@ void ScalePicker::setPen(QPen pen) } // convert the position of the click to the plot coordinates -double ScalePicker::position(double posPx) +double ScalePicker::position(double posPx) const { return _scaleWidget->scaleDraw()->scaleMap().invTransform(posPx); } @@ -248,7 +418,7 @@ int ScalePicker::positionPx(QMouseEvent* * when drawing the tracker lines. This function maps scale widgets * pixel coordinate to canvas' coordinate. */ -double ScalePicker::posCanvasPx(double pos) +double ScalePicker::posCanvasPx(double pos) const { // assumption: scale.width < canvas.width && scale.x > canvas.x if (_scaleWidget->alignment() == QwtScaleDraw::BottomScale || @@ -270,9 +440,12 @@ void ScalePicker::updateSnapPoints() _scaleWidget->scaleDraw()->scaleDiv().ticks(QwtScaleDiv::MinorTick); snapPoints.clear(); + snapPointMap.clear(); for(auto t : allTicks) { // `round` is used because `allTicks` is double but `snapPoints` is int - snapPoints << round(_scaleWidget->scaleDraw()->scaleMap().transform(t)); + int p = round(_scaleWidget->scaleDraw()->scaleMap().transform(t)); + snapPoints << p; + snapPointMap[p] = t; } } diff --git a/src/scalepicker.h b/src/scalepicker.h --- a/src/scalepicker.h +++ b/src/scalepicker.h @@ -1,5 +1,5 @@ /* - Copyright © 2015 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -41,8 +42,6 @@ public: void setPen(QPen pen); signals: - void pickStarted(double pos); - void picking(double firstPos, double lastPos); void picked(double firstPos, double lastPos); private: @@ -58,10 +57,18 @@ private: double firstPosPx; // pixel coordinates double currentPosPx; // current position in pixel coordinates QList snapPoints; + /// used to restore precision of snappoints that is lost due to rounding + QMap snapPointMap; - double position(double); // returns the axis mouse position relative to plot coordinates + double position(double) const; // returns the axis mouse position relative to plot coordinates int positionPx(QMouseEvent*); // returns the axis mouse position in pixels - double posCanvasPx(double pos); // returns the given position in canvas coordinates + double posCanvasPx(double pos) const; // returns the given position in canvas coordinates + void drawTriangle(QPainter* painter, int position); + QwtText trackerText() const; + /// Returns tracker text position + QRectF trackerTextRect(QPainter* painter, int posPx, QSizeF textSize) const; + /// Returns the text position for tracker text shown during picking + QRectF pickTrackerTextRect(QPainter* painter, QRect pickRect, QSizeF textSize) const; private slots: void updateSnapPoints(); diff --git a/src/setting_defines.h b/src/setting_defines.h --- a/src/setting_defines.h +++ b/src/setting_defines.h @@ -30,6 +30,7 @@ const char SettingGroup_Channels[] = "Ch const char SettingGroup_Plot[] = "Plot"; const char SettingGroup_Commands[] = "Commands"; const char SettingGroup_Record[] = "Record"; +const char SettingGroup_UpdateCheck[] = "UpdateCheck"; // mainwindow setting keys const char SG_MainWindow_Size[] = "size"; @@ -57,6 +58,8 @@ const char SG_Binary_Endianness[] = "end // ascii reader keys const char SG_ASCII_NumOfChannels[] = "numOfChannels"; +const char SG_ASCII_Delimiter[] = "delimiter"; +const char SG_ASCII_CustomDelimiter[] = "customDelimiter"; // framed reader keys const char SG_CustomFrame_NumOfChannels[] = "numOfChannels"; @@ -76,6 +79,9 @@ const char SG_Channels_Visible[] = "visi // plot settings keys const char SG_Plot_NumOfSamples[] = "numOfSamples"; +const char SG_Plot_IndexAsX[] = "indexAsX"; +const char SG_Plot_XMax[] = "xMax"; +const char SG_Plot_XMin[] = "xMin"; const char SG_Plot_AutoScale[] = "autoScale"; const char SG_Plot_YMax[] = "yMax"; const char SG_Plot_YMin[] = "yMin"; @@ -84,6 +90,7 @@ const char SG_Plot_Grid[] = "grid"; const char SG_Plot_MinorGrid[] = "minorGrid"; const char SG_Plot_Legend[] = "legend"; const char SG_Plot_MultiPlot[] = "multiPlot"; +const char SG_Plot_Symbols[] = "symbols"; // command setting keys const char SG_Commands_Command[] = "command"; @@ -99,4 +106,8 @@ const char SG_Record_Header[] const char SG_Record_Separator[] = "separator"; const char SG_Record_DisableBuffering[] = "disableBuffering"; +// update check settings keys +const char SG_UpdateCheck_Periodic[] = "periodicCheck"; +const char SG_UpdateCheck_LastCheck[] = "lastCheck"; + #endif // SETTING_DEFINES_H diff --git a/src/snapshot.cpp b/src/snapshot.cpp --- a/src/snapshot.cpp +++ b/src/snapshot.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2015 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -21,17 +21,18 @@ #include #include +#include "mainwindow.h" #include "snapshot.h" #include "snapshotview.h" -Snapshot::Snapshot(QMainWindow* parent, QString name, ChannelInfoModel infoModel) : +Snapshot::Snapshot(MainWindow* parent, QString name, ChannelInfoModel infoModel, bool saved) : QObject(parent), cInfoModel(infoModel), _showAction(this), _deleteAction("&Delete", this) { _name = name; - _saved = false; + _saved = saved; view = NULL; mainWindow = parent; diff --git a/src/snapshot.h b/src/snapshot.h --- a/src/snapshot.h +++ b/src/snapshot.h @@ -1,5 +1,5 @@ /* - Copyright © 2015 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -21,7 +21,6 @@ #define SNAPSHOT_H #include -#include #include #include #include @@ -30,13 +29,14 @@ #include "channelinfomodel.h" class SnapshotView; +class MainWindow; class Snapshot : public QObject { Q_OBJECT public: - Snapshot(QMainWindow* parent, QString name, ChannelInfoModel infoModel); + Snapshot(MainWindow* parent, QString name, ChannelInfoModel infoModel, bool saved = false); ~Snapshot(); QVector> data; @@ -61,7 +61,7 @@ private: ChannelInfoModel cInfoModel; QAction _showAction; QAction _deleteAction; - QMainWindow* mainWindow; + MainWindow* mainWindow; SnapshotView* view; bool _saved; diff --git a/src/snapshotmanager.cpp b/src/snapshotmanager.cpp --- a/src/snapshotmanager.cpp +++ b/src/snapshotmanager.cpp @@ -24,11 +24,13 @@ #include #include #include +#include #include +#include "mainwindow.h" #include "snapshotmanager.h" -SnapshotManager::SnapshotManager(QMainWindow* mainWindow, +SnapshotManager::SnapshotManager(MainWindow* mainWindow, ChannelManager* channelMan) : _menu("&Snapshots"), _takeSnapshotAction("&Take Snapshot", this), @@ -40,6 +42,7 @@ SnapshotManager::SnapshotManager(QMainWi _takeSnapshotAction.setToolTip("Take a snapshot of current plot"); _takeSnapshotAction.setShortcut(QKeySequence("F5")); + _takeSnapshotAction.setIcon(QIcon::fromTheme("camera")); loadSnapshotAction.setToolTip("Load snapshots from CSV files"); clearAction.setToolTip("Delete all snapshots"); connect(&_takeSnapshotAction, SIGNAL(triggered(bool)), @@ -191,7 +194,8 @@ void SnapshotManager::loadSnapshotFromFi ChannelInfoModel channelInfo(channelNames); auto snapshot = new Snapshot( - _mainWindow, QFileInfo(fileName).baseName(), ChannelInfoModel(channelNames)); + _mainWindow, QFileInfo(fileName).baseName(), + ChannelInfoModel(channelNames), true); snapshot->data = data; addSnapshot(snapshot, false); diff --git a/src/snapshotmanager.h b/src/snapshotmanager.h --- a/src/snapshotmanager.h +++ b/src/snapshotmanager.h @@ -1,5 +1,5 @@ /* - Copyright © 2015 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -28,12 +28,14 @@ #include "channelmanager.h" #include "snapshot.h" +class MainWindow; + class SnapshotManager : public QObject { Q_OBJECT public: - SnapshotManager(QMainWindow* mainWindow, ChannelManager* channelMan); + SnapshotManager(MainWindow* mainWindow, ChannelManager* channelMan); ~SnapshotManager(); QMenu* menu(); @@ -46,7 +48,7 @@ public: bool isAllSaved(); ///< returns `true` if all snapshots are saved to a file private: - QMainWindow* _mainWindow; + MainWindow* _mainWindow; ChannelManager* _channelMan; QList snapshots; diff --git a/src/snapshotview.cpp b/src/snapshotview.cpp --- a/src/snapshotview.cpp +++ b/src/snapshotview.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2015 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -20,7 +20,7 @@ #include "snapshotview.h" #include "ui_snapshotview.h" -SnapshotView::SnapshotView(QWidget *parent, Snapshot* snapshot) : +SnapshotView::SnapshotView(MainWindow* parent, Snapshot* snapshot) : QMainWindow(parent), ui(new Ui::SnapshotView), renameDialog(this) @@ -29,17 +29,20 @@ SnapshotView::SnapshotView(QWidget *pare ui->setupUi(this); - plotMan = new PlotManager(ui->plotArea, snapshot->infoModel()); + plotMan = new PlotManager(ui->plotArea, snapshot->infoModel(), this); + plotMan->setViewSettings(parent->viewSettings()); ui->menuSnapshot->insertAction(ui->actionClose, snapshot->deleteAction()); this->setWindowTitle(snapshot->displayName()); // initialize curves unsigned numOfChannels = snapshot->data.size(); + unsigned numOfSamples = snapshot->data[0].size(); for (unsigned ci = 0; ci < numOfChannels; ci++) { plotMan->addCurve(snapshot->channelName(ci), snapshot->data[ci]); } + plotMan->setNumOfSamples(numOfSamples); renameDialog.setWindowTitle("Rename Snapshot"); renameDialog.setLabelText("Enter new name:"); @@ -62,6 +65,7 @@ SnapshotView::~SnapshotView() { delete curve; } + delete plotMan; delete ui; } diff --git a/src/snapshotview.h b/src/snapshotview.h --- a/src/snapshotview.h +++ b/src/snapshotview.h @@ -1,5 +1,5 @@ /* - Copyright © 2015 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -29,6 +29,7 @@ #include #include +#include "mainwindow.h" #include "plotmanager.h" #include "snapshot.h" @@ -41,7 +42,7 @@ class SnapshotView : public QMainWindow Q_OBJECT public: - explicit SnapshotView(QWidget *parent, Snapshot* snapshot); + explicit SnapshotView(MainWindow* parent, Snapshot* snapshot); ~SnapshotView(); signals: diff --git a/src/updatecheckdialog.cpp b/src/updatecheckdialog.cpp new file mode 100644 --- /dev/null +++ b/src/updatecheckdialog.cpp @@ -0,0 +1,105 @@ +/* + Copyright © 2017 Hasan Yavuz Özderya + + This file is part of serialplot. + + serialplot is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + serialplot 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 serialplot. If not, see . +*/ + +#include "setting_defines.h" +#include "updatecheckdialog.h" +#include "ui_updatecheckdialog.h" + +UpdateCheckDialog::UpdateCheckDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::UpdateCheckDialog) +{ + ui->setupUi(this); + + // by default start from yesterday, so that we check at first run + lastCheck = QDate::currentDate().addDays(-1); + + connect(&updateChecker, &UpdateChecker::checkFailed, + [this](QString errorMessage) + { + lastCheck = QDate::currentDate(); + ui->label->setText(QString("Update check failed.\n") + errorMessage); + }); + + connect(&updateChecker, &UpdateChecker::checkFinished, + [this](bool found, QString newVersion, QString downloadUrl) + { + QString text; + if (!found) + { + text = "There is no update yet."; + } + else + { + show(); +#ifdef UPDATE_TYPE_PKGMAN + text = QString("There is a new version: %1. " + "Use your package manager to update" + " or click to download.")\ + .arg(newVersion).arg(downloadUrl); +#else + text = QString("Found update to version %1. Click to download.")\ + .arg(newVersion).arg(downloadUrl); +#endif + } + + lastCheck = QDate::currentDate(); + ui->label->setText(text); + }); +} + +UpdateCheckDialog::~UpdateCheckDialog() +{ + delete ui; +} + +void UpdateCheckDialog::showEvent(QShowEvent *event) +{ + updateChecker.checkUpdate(); + ui->label->setText("Checking update..."); +} + +void UpdateCheckDialog::closeEvent(QShowEvent *event) +{ + if (updateChecker.isChecking()) updateChecker.cancelCheck(); +} + +void UpdateCheckDialog::saveSettings(QSettings* settings) +{ + settings->beginGroup(SettingGroup_UpdateCheck); + settings->setValue(SG_UpdateCheck_Periodic, ui->cbPeriodic->isChecked()); + settings->setValue(SG_UpdateCheck_LastCheck, lastCheck.toString(Qt::ISODate)); + settings->endGroup(); +} + +void UpdateCheckDialog::loadSettings(QSettings* settings) +{ + settings->beginGroup(SettingGroup_UpdateCheck); + ui->cbPeriodic->setChecked(settings->value(SG_UpdateCheck_Periodic, + ui->cbPeriodic->isChecked()).toBool()); + auto lastCheckS = settings->value(SG_UpdateCheck_LastCheck, lastCheck.toString(Qt::ISODate)).toString(); + lastCheck = QDate::fromString(lastCheckS, Qt::ISODate); + settings->endGroup(); + + // start the periodic update if required + if (ui->cbPeriodic->isChecked() && lastCheck < QDate::currentDate()) + { + updateChecker.checkUpdate(); + } +} diff --git a/src/updatecheckdialog.h b/src/updatecheckdialog.h new file mode 100644 --- /dev/null +++ b/src/updatecheckdialog.h @@ -0,0 +1,54 @@ +/* + Copyright © 2017 Hasan Yavuz Özderya + + This file is part of serialplot. + + serialplot is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + serialplot 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 serialplot. If not, see . +*/ + +#ifndef UPDATECHECKDIALOG_H +#define UPDATECHECKDIALOG_H + +#include +#include +#include +#include "updatechecker.h" + +namespace Ui { +class UpdateCheckDialog; +} + +class UpdateCheckDialog : public QDialog +{ + Q_OBJECT + +public: + explicit UpdateCheckDialog(QWidget *parent = 0); + ~UpdateCheckDialog(); + + /// Stores update settings into a `QSettings`. + void saveSettings(QSettings* settings); + /// Loads update settings from a `QSettings`. + void loadSettings(QSettings* settings); + +private: + Ui::UpdateCheckDialog *ui; + UpdateChecker updateChecker; + QDate lastCheck; + + void showEvent(QShowEvent *event); + void closeEvent(QShowEvent *event); +}; + +#endif // UPDATECHECKDIALOG_H diff --git a/src/updatecheckdialog.ui b/src/updatecheckdialog.ui new file mode 100644 --- /dev/null +++ b/src/updatecheckdialog.ui @@ -0,0 +1,71 @@ + + + UpdateCheckDialog + + + + 0 + 0 + 400 + 148 + + + + Check Update + + + + + + Checking update... + + + true + + + + + + + Updates will be checked only once a day at first start of the application + + + Check updates periodically + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + + buttonBox + clicked(QAbstractButton*) + UpdateCheckDialog + close() + + + 199 + 125 + + + 199 + 73 + + + + + diff --git a/src/updatechecker.cpp b/src/updatechecker.cpp new file mode 100644 --- /dev/null +++ b/src/updatechecker.cpp @@ -0,0 +1,222 @@ +/* + Copyright © 2017 Hasan Yavuz Özderya + + This file is part of serialplot. + + serialplot is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + serialplot 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 serialplot. If not, see . +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include "updatechecker.h" + +// This link returns the list of downloads in JSON format. Note that we only use +// the first page because results are sorted new to old. +const char BB_DOWNLOADS_URL[] = "https://api.bitbucket.org/2.0/repositories/hyozd/serialplot/downloads?fields=values.name,values.links.self.href"; + +UpdateChecker::UpdateChecker(QObject *parent) : + QObject(parent), nam(this) +{ + activeReply = NULL; + + connect(&nam, &QNetworkAccessManager::finished, + this, &UpdateChecker::onReqFinished); +} + +bool UpdateChecker::isChecking() const +{ + return activeReply != NULL && !activeReply->isFinished(); +} + +void UpdateChecker::checkUpdate() +{ + if (isChecking()) return; + + auto req = QNetworkRequest(QUrl(BB_DOWNLOADS_URL)); + activeReply = nam.get(req); +} + +void UpdateChecker::cancelCheck() +{ + if (activeReply != NULL) activeReply->abort(); +} + +void UpdateChecker::onReqFinished(QNetworkReply* reply) +{ + if (reply->error() != QNetworkReply::NoError) + { + emit checkFailed(QString("Network error: ") + reply->errorString()); + } + else + { + QJsonParseError error; + auto data = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + emit checkFailed(QString("JSon parsing error: ") + error.errorString()); + } + else + { + QList files; + if (!parseData(data, files)) + { + // TODO: emit detailed data contents for logging + emit checkFailed("Data parsing error."); + } + else + { + FileInfo updateFile; + if (findUpdate(files, updateFile)) + { + emit checkFinished( + true, updateFile.version.toString(), updateFile.link); + } + else + { + emit checkFinished(false, "", ""); + } + } + } + } + reply->deleteLater(); + activeReply = NULL; +} + +bool UpdateChecker::parseData(const QJsonDocument& data, QList& files) const +{ + /* Data is expected to be in this form: + + { + "values": [ + { + "name": "serialplot-0.9.1-x86_64.AppImage", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hyOzd/serialplot/downloads/serialplot-0.9.1-x86_64.AppImage" + } + } + }, ... ] + } + */ + + if (!data.isObject()) return false; + + auto values = data.object()["values"]; + if (values == QJsonValue::Undefined || !values.isArray()) return false; + + for (auto value : values.toArray()) + { + if (!value.isObject()) return false; + + auto name = value.toObject().value("name"); + if (name.isUndefined() || !name.isString()) + return false; + + auto links = value.toObject().value("links"); + if (links.isUndefined() || !links.isObject()) + return false; + + auto self = links.toObject().value("self"); + if (self.isUndefined() || !self.isObject()) + return false; + + auto href = self.toObject().value("href"); + if (href.isUndefined() || !href.isString()) + return false; + + FileInfo finfo; + finfo.name = name.toString(); + finfo.link = href.toString(); + finfo.hasVersion = VersionNumber::extract(name.toString(), finfo.version); + + if (finfo.name.contains("amd64") || + finfo.name.contains("x86_64") || + finfo.name.contains("win64")) + { + finfo.arch = FileArch::amd64; + } + else if (finfo.name.contains("win32") || + finfo.name.contains("i386")) + { + finfo.arch = FileArch::i386; + } + else + { + finfo.arch = FileArch::unknown; + } + + files += finfo; + } + + return true; +} + +bool UpdateChecker::findUpdate(const QList& files, FileInfo& foundFile) const +{ + QList fflist; + + // filter the file list according to extension and version number + for (int i = 0; i < files.length(); i++) + { + // file type to look +#if defined(Q_OS_WIN) + const char ext[] = ".exe"; +#else // of course linux + const char ext[] = ".appimage"; +#endif + + // file architecture to look +#if defined(Q_PROCESSOR_X86_64) + const FileArch arch = FileArch::amd64; +#elif defined(Q_PROCESSOR_X86_32) + const FileArch arch = FileArch::i386; +#elif defined(Q_PROCESSOR_ARM) + const FileArch arch = FileArch::arm; +#else + #error Unknown architecture for update file detection. +#endif + + // filter the file list + auto file = files[i]; + if (file.name.contains(ext, Qt::CaseInsensitive) && + file.arch == arch && + file.hasVersion && file.version > CurrentVersion) + { + fflist += file; + } + } + + // sort and find most up to date file + if (!fflist.empty()) + { + std::sort(fflist.begin(), fflist.end(), + [](const FileInfo& a, const FileInfo& b) + { + return a.version > b.version; + }); + + foundFile = fflist[0]; + return true; + } + else + { + return false; + } +} diff --git a/src/updatechecker.h b/src/updatechecker.h new file mode 100644 --- /dev/null +++ b/src/updatechecker.h @@ -0,0 +1,77 @@ +/* + Copyright © 2017 Hasan Yavuz Özderya + + This file is part of serialplot. + + serialplot is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + serialplot 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 serialplot. If not, see . +*/ + +#ifndef UPDATECHECKER_H +#define UPDATECHECKER_H + +#include +#include +#include +#include + +#include "versionnumber.h" + +class UpdateChecker : public QObject +{ + Q_OBJECT +public: + explicit UpdateChecker(QObject *parent = 0); + + bool isChecking() const; + +signals: + void checkFinished(bool found, QString newVersion, QString downloadUrl); + void checkFailed(QString errorMessage); + +public slots: + void checkUpdate(); + void cancelCheck(); + +private: + enum class FileArch + { + unknown, + i386, + amd64, + arm + }; + + struct FileInfo + { + QString name; + QString link; + bool hasVersion; + VersionNumber version; + FileArch arch; + }; + + QNetworkAccessManager nam; + QNetworkReply* activeReply; + + /// Parses json and creates a list of files + bool parseData(const QJsonDocument& data, QList& files) const; + /// Finds the update file in the file list. Returns `-1` if no new version + /// is found. + bool findUpdate(const QList& files, FileInfo& foundFile) const; + +private slots: + void onReqFinished(QNetworkReply* reply); +}; + +#endif // UPDATECHECKER_H diff --git a/src/version.h b/src/version.h new file mode 100644 --- /dev/null +++ b/src/version.h @@ -0,0 +1,32 @@ +/* + Copyright © 2017 Hasan Yavuz Özderya + + This file is part of serialplot. + + serialplot is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + serialplot 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 serialplot. If not, see . +*/ + +#ifndef VERSION_H +#define VERSION_H + +#ifndef VERSION_STRING +#warning VERSION_STRING not defined! +#define VERSION_STRING "0.0.0" +#endif + +#ifndef VERSION_REVISION +#define VERSION_REVISION "" +#endif + +#endif // VERSION_H diff --git a/src/version.h.in b/src/version.h.in --- a/src/version.h.in +++ b/src/version.h.in @@ -1,5 +1,5 @@ /* - Copyright © 2015 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -20,9 +20,6 @@ #ifndef VERSION_H #define VERSION_H -#define MAJOR_VERSION @MAJOR_VERSION@ -#define MINOR_VERSION @MINOR_VERSION@ -#define PATCH_VERSION @PATCH_VERSION@ #define VERSION_STRING "@VERSION_STRING@" #define VERSION_REVISION "@VERSION_REVISION@" diff --git a/src/versionnumber.cpp b/src/versionnumber.cpp new file mode 100644 --- /dev/null +++ b/src/versionnumber.cpp @@ -0,0 +1,103 @@ +/* + Copyright © 2017 Hasan Yavuz Özderya + + This file is part of serialplot. + + serialplot is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + serialplot 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 serialplot. If not, see . +*/ + +#include + +#include "versionnumber.h" + +VersionNumber::VersionNumber(unsigned mj, unsigned mn, unsigned pt) +{ + major = mj; + minor = mn; + patch = pt; +} + +QString VersionNumber::toString() const +{ + return QString("%1.%2.%3").arg(major).arg(minor).arg(patch); +} + +bool VersionNumber::extract(const QString& str, VersionNumber& number) +{ + QRegularExpression regexp("(?:[-_vV \\t]|^)(?\\d+)" + "(?:\\.(?\\d+))(?:\\.(?\\d+))?[-_ \\t]?"); + auto match = regexp.match(str, 0, QRegularExpression::PartialPreferCompleteMatch); + + if (!(match.hasMatch() || match.hasPartialMatch())) return false; + + number.major = match.captured("major").toUInt(); + + auto zeroIfNull = [](QString str) -> unsigned + { + if (str.isNull()) return 0; + return str.toUInt(); + }; + + number.minor = zeroIfNull(match.captured("minor")); + number.patch = zeroIfNull(match.captured("patch")); + + return true; +} + +bool operator==(const VersionNumber& lhs, const VersionNumber& rhs) +{ + return lhs.major == rhs.major && + lhs.minor == rhs.minor && + lhs.patch == rhs.patch; +} + +bool operator<(const VersionNumber& lhs, const VersionNumber& rhs) +{ + if (lhs.major < rhs.major) + { + return true; + } + else if (lhs.major == rhs.major) + { + if (lhs.minor < rhs.minor) + { + return true; + } + else if (lhs.minor == rhs.minor) + { + if (lhs.patch < rhs.patch) return true; + } + } + return false; +} + +bool operator>(const VersionNumber& lhs, const VersionNumber& rhs) +{ + if (lhs.major > rhs.major) + { + return true; + } + else if (lhs.major == rhs.major) + { + if (lhs.minor > rhs.minor) + { + return true; + } + else if (lhs.minor == rhs.minor) + { + if (lhs.patch > rhs.patch) return true; + } + } + return false; +} diff --git a/src/versionnumber.h b/src/versionnumber.h new file mode 100644 --- /dev/null +++ b/src/versionnumber.h @@ -0,0 +1,46 @@ +/* + Copyright © 2017 Hasan Yavuz Özderya + + This file is part of serialplot. + + serialplot is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + serialplot 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 serialplot. If not, see . +*/ + +#ifndef VERSIONNUMBER_H +#define VERSIONNUMBER_H + +#include + +struct VersionNumber +{ + unsigned major = 0; + unsigned minor = 0; + unsigned patch = 0; + + VersionNumber(unsigned mj=0, unsigned mn=0, unsigned pt=0); + + /// Convert version number to string. + QString toString() const; + + /// Extracts the version number from given string. + static bool extract(const QString& str, VersionNumber& number); +}; + +bool operator==(const VersionNumber& lhs, const VersionNumber& rhs); +bool operator<(const VersionNumber& lhs, const VersionNumber& rhs); +bool operator>(const VersionNumber& lhs, const VersionNumber& rhs); + +const VersionNumber CurrentVersion(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); + +#endif // VERSIONNUMBER_H diff --git a/src/zoomer.cpp b/src/zoomer.cpp --- a/src/zoomer.cpp +++ b/src/zoomer.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -21,9 +21,13 @@ #include #include +#include + Zoomer::Zoomer(QWidget* widget, bool doReplot) : ScrollZoomer(widget) { + is_panning = false; + // set corner widget between the scrollbars with default background color auto cornerWidget = new QWidget(); auto bgColor = cornerWidget->palette().color(QPalette::Window).name(); @@ -52,3 +56,88 @@ void Zoomer::zoom( const QRectF & rect) ScrollZoomer::zoom(rect); } + +QwtText Zoomer::trackerTextF(const QPointF& pos) const +{ + QwtText b = ScrollZoomer::trackerTextF(pos); + + const QPolygon pa = selection(); + if (pa.count() < 2) + { + return b; + } + + const QRectF rect = invTransform(QRect(pa.first(), pa.last()).normalized()); + + QString sizeText = QString(" [%1, %2]").\ + arg(rect.width(), 0, 'g', 4).\ + arg(rect.height(), 0, 'g', 4); + + b.setText(b.text() + sizeText); + + return b; +} + +void Zoomer::drawRubberBand(QPainter* painter) const +{ + const double FILL_ALPHA = 0.2; + + QColor color = painter->pen().color(); + color.setAlphaF(FILL_ALPHA); + painter->setBrush(color); + + ScrollZoomer::drawRubberBand(painter); +} + +QRegion Zoomer::rubberBandMask() const +{ + const QPolygon pa = selection(); + if (pa.count() < 2) + { + return QRegion(); + } + const QRect r = QRect(pa.first(), pa.last()).normalized().adjusted(0, 0, 1, 1); + return QRegion(r); +} + +void Zoomer::widgetMousePressEvent(QMouseEvent* mouseEvent) +{ + if (mouseEvent->modifiers() & Qt::ControlModifier) + { + is_panning = true; + parentWidget()->setCursor(Qt::ClosedHandCursor); + pan_point = invTransform(mouseEvent->pos()); + } + else + { + ScrollZoomer::widgetMousePressEvent(mouseEvent); + } +} + +void Zoomer::widgetMouseMoveEvent(QMouseEvent* mouseEvent) +{ + if (is_panning) + { + auto cur_point = invTransform(mouseEvent->pos()); + auto delta = cur_point - pan_point; + moveBy(-delta.x(), -delta.y()); + pan_point = invTransform(mouseEvent->pos()); + } + else + { + ScrollZoomer::widgetMouseMoveEvent(mouseEvent); + } +} + +void Zoomer::widgetMouseReleaseEvent(QMouseEvent* mouseEvent) +{ + if (is_panning) + { + is_panning = false; + parentWidget()->setCursor(Qt::CrossCursor); + } + else + { + ScrollZoomer::widgetMouseReleaseEvent(mouseEvent); + } +} diff --git a/src/zoomer.h b/src/zoomer.h --- a/src/zoomer.h +++ b/src/zoomer.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -33,6 +33,24 @@ public: signals: void unzoomed(); + +protected: + /// Re-implemented to display selection size in the tracker text. + QwtText trackerTextF(const QPointF &pos) const; + /// Re-implemented for alpha background + void drawRubberBand(QPainter* painter) const; + /// Re-implemented for alpha background (masking is basically disabled) + QRegion rubberBandMask() const; + /// Overloaded for panning + void widgetMousePressEvent(QMouseEvent* mouseEvent); + /// Overloaded for panning + void widgetMouseReleaseEvent(QMouseEvent* mouseEvent); + /// Overloaded for panning + void widgetMouseMoveEvent(QMouseEvent* mouseEvent); + +private: + bool is_panning; + QPointF pan_point; }; #endif // ZOOMER_H