# HG changeset patch # User Hasan Yavuz ÖZDERYA # Date 2017-04-24 14:20:22 # Node ID c374f20e3844e31f5614c6fb4b6380af074ddba8 # Parent c78aec692ac0b85831105529a69b663c5b3cf33e # Parent dbb380c5cb059e96c55e0d0c8815b8428f70abb0 Merge with default diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -9,3 +9,4 @@ b4d0a38444d31872633e474d89ffc15cd0fe42f0 27b0354ca2c5ea7b3870156417ce7e04e799bbf7 v0.7.1 fd5f1eb480ec372b49df58b497458de05c30057c v0.8.0 9c9a11cd15fd094e2b2b65dc51805fd8fd1d2460 v0.8.1 +4cf9a1ee1f107a38e03dbe17c4f2882c43d827c9 v0.9.0 diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright © 2015-2016 Hasan Yavuz Özderya +# Copyright © 2017 Hasan Yavuz Özderya # # This file is part of serialplot. # @@ -38,24 +38,28 @@ 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) +# 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 () + # includes -include_directories("./src" ${QWT_INCLUDE_DIR}) +include_directories("./src" + ${QWT_INCLUDE_DIR} + ${QTCOLORWIDGETS_INCLUDE_DIRS} + ) + +# flags +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${QTCOLORWIDGETS_FLAGS}") # wrap UI and resource files qt5_wrap_ui(UI_FILES @@ -67,6 +71,7 @@ qt5_wrap_ui(UI_FILES src/commandwidget.ui src/dataformatpanel.ui src/plotcontrolpanel.ui + src/recordpanel.ui src/numberformatbox.ui src/endiannessbox.ui src/binarystreamreadersettings.ui @@ -102,9 +107,12 @@ add_executable(${PROGRAM_NAME} WIN32 src/commandedit.cpp src/dataformatpanel.cpp src/plotcontrolpanel.cpp + src/recordpanel.cpp + src/datarecorder.cpp src/tooltipfilter.cpp src/sneakylineedit.cpp src/channelmanager.cpp + src/channelinfomodel.cpp src/framebufferseries.cpp src/numberformatbox.cpp src/endiannessbox.cpp @@ -124,12 +132,19 @@ add_executable(${PROGRAM_NAME} WIN32 ) # Use the Widgets module from Qt 5. -target_link_libraries(${PROGRAM_NAME} ${QWT_LIBRARY}) -qt5_use_modules(${PROGRAM_NAME} Widgets SerialPort Svg) +target_link_libraries(${PROGRAM_NAME} + ${QWT_LIBRARY} + ${QTCOLORWIDGETS_LIBRARIES} + ) +qt5_use_modules(${PROGRAM_NAME} Widgets SerialPort) if (BUILD_QWT) add_dependencies(${PROGRAM_NAME} QWT) -endif (BUILD_QWT) +endif () + +if (BUILD_QTCOLORWIDGETS) + add_dependencies(${PROGRAM_NAME} QCW) +endif () # set compiler flags set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") @@ -146,12 +161,6 @@ 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) @@ -171,9 +180,7 @@ if (NOT VERSION_REVISION) 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_REVISION=\\\"${VERSION_REVISION}\\\" ") # add make run target add_custom_target(run diff --git a/cmake/modules/BuildQColorWidgets.cmake b/cmake/modules/BuildQColorWidgets.cmake new file mode 100644 --- /dev/null +++ b/cmake/modules/BuildQColorWidgets.cmake @@ -0,0 +1,36 @@ +# +# 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(ExternalProject) + +ExternalProject_Add(QCW + PREFIX qcw + GIT_REPOSITORY https://github.com/mbasaglia/Qt-Color-Widgets + 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(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/BuildQwt.cmake b/cmake/modules/BuildQwt.cmake --- a/cmake/modules/BuildQwt.cmake +++ b/cmake/modules/BuildQwt.cmake @@ -25,8 +25,11 @@ ExternalProject_Add(QWT # disable QwtDesigner plugin and enable static build PATCH_COMMAND sed -i -r -e "s/QWT_CONFIG\\s*\\+=\\s*QwtDesigner/#&/" -e "s/QWT_CONFIG\\s*\\+=\\s*QwtDll/#&/" + -e "s/QWT_CONFIG\\s*\\+=\\s*QwtSvg/#&/" + -e "s/QWT_CONFIG\\s*\\+=\\s*QwtOpenGL/#&/" -e "s|QWT_INSTALL_PREFIX\\s*=.*|QWT_INSTALL_PREFIX = |" - /qwtconfig.pri + /qwtconfig.pri + UPDATE_COMMAND "" CONFIGURE_COMMAND qmake /qwt.pro ) 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/qt_5_2_moc_creation_namespace_fix.diff b/cmake/modules/qt_5_2_moc_creation_namespace_fix.diff new file mode 100644 --- /dev/null +++ b/cmake/modules/qt_5_2_moc_creation_namespace_fix.diff @@ -0,0 +1,39 @@ +diff --git a/include/color_dialog.hpp b/include/color_dialog.hpp +index 5c7653d..895215c 100644 +--- a/include/color_dialog.hpp ++++ b/include/color_dialog.hpp +@@ -30,6 +30,8 @@ + + class QAbstractButton; + ++using namespace color_widgets; ++ + namespace color_widgets { + + class QCP_EXPORT ColorDialog : public QDialog +diff --git a/include/color_list_widget.hpp b/include/color_list_widget.hpp +index 282bea5..7d8e0c5 100644 +--- a/include/color_list_widget.hpp ++++ b/include/color_list_widget.hpp +@@ -25,6 +25,8 @@ + #include "abstract_widget_list.hpp" + #include "color_wheel.hpp" + ++using namespace color_widgets; ++ + namespace color_widgets { + + class QCP_EXPORT ColorListWidget : public AbstractWidgetList +diff --git a/include/color_selector.hpp b/include/color_selector.hpp +index db817d5..48b374d 100644 +--- a/include/color_selector.hpp ++++ b/include/color_selector.hpp +@@ -25,6 +25,8 @@ + #include "color_preview.hpp" + #include "color_wheel.hpp" + ++using namespace color_widgets; ++ + namespace color_widgets { + + /** diff --git a/serialplot.pro b/serialplot.pro --- a/serialplot.pro +++ b/serialplot.pro @@ -67,7 +67,8 @@ SOURCES += \ src/demoreader.cpp \ src/framedreader.cpp \ src/plotmanager.cpp \ - src/numberformat.cpp + src/numberformat.cpp \ + src/recordpanel.cpp HEADERS += \ src/mainwindow.h \ @@ -106,7 +107,8 @@ HEADERS += \ src/framedreader.h \ src/plotmanager.h \ src/setting_defines.h \ - src/numberformat.h + src/numberformat.h \ + src/recordpanel.h FORMS += \ src/mainwindow.ui \ @@ -121,7 +123,8 @@ FORMS += \ src/endiannessbox.ui \ src/framedreadersettings.ui \ src/binarystreamreadersettings.ui \ - src/asciireadersettings.ui + src/asciireadersettings.ui \ + src/recordpanel.ui INCLUDEPATH += qmake/ src/ diff --git a/src/abstractreader.cpp b/src/abstractreader.cpp --- a/src/abstractreader.cpp +++ b/src/abstractreader.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -19,11 +19,14 @@ #include "abstractreader.h" -AbstractReader::AbstractReader(QIODevice* device, ChannelManager* channelMan, QObject *parent) : +AbstractReader::AbstractReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject* parent) : QObject(parent) { _device = device; _channelMan = channelMan; + _recorder = recorder; + recording = false; // initialize sps counter sampleCount = 0; @@ -44,3 +47,13 @@ void AbstractReader::spsTimerTimeout() } sampleCount = 0; } + +void AbstractReader::addData(double* samples, unsigned length) +{ + _channelMan->addData(samples, length); + if (recording) + { + _recorder->addData(samples, length, numOfChannels()); + } + sampleCount += length; +} diff --git a/src/abstractreader.h b/src/abstractreader.h --- a/src/abstractreader.h +++ b/src/abstractreader.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -26,6 +26,7 @@ #include #include "channelmanager.h" +#include "datarecorder.h" /** * All reader classes must inherit this class. @@ -34,7 +35,10 @@ class AbstractReader : public QObject { Q_OBJECT public: - explicit AbstractReader(QIODevice* device, ChannelManager* channelMan, QObject *parent = 0); + explicit AbstractReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject* parent = 0); + + bool recording; /// is recording started /** * Returns a widget to be shown in data format panel when reader @@ -54,11 +58,18 @@ public: /// 'disabled'. virtual void enable(bool enabled = true) = 0; + /** + * @brief Starts sending data to recorder. + * + * @note recorder must have been started! + */ + void startRecording(); + + /// Stops recording. + void stopRecording(); + signals: void numOfChannelsChanged(unsigned); - // TODO: this must be signaled by 'channel man' for better abstraction - void dataAdded(); ///< emitted when data added to channel man. - // TODO: this should be a part of 'channel man' void samplesPerSecondChanged(unsigned); public slots: @@ -72,14 +83,18 @@ public slots: protected: QIODevice* _device; - ChannelManager* _channelMan; - /// Implementing class should simply increase this count as samples are read - unsigned sampleCount; + /// Should be called with read data + void addData(double* samples, unsigned length); private: const int SPS_UPDATE_TIMEOUT = 1; // second + + unsigned sampleCount; unsigned samplesPerSecond; + + ChannelManager* _channelMan; + DataRecorder* _recorder; QTimer spsTimer; private slots: diff --git a/src/asciireader.cpp b/src/asciireader.cpp --- a/src/asciireader.cpp +++ b/src/asciireader.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -24,12 +24,12 @@ /// If set to this value number of channels is determined from input #define NUMOFCHANNELS_AUTO (0) -AsciiReader::AsciiReader(QIODevice* device, ChannelManager* channelMan, QObject *parent) : - AbstractReader(device, channelMan, parent) +AsciiReader::AsciiReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject* parent) : + AbstractReader(device, channelMan, recorder, parent) { paused = false; discardFirstLine = true; - sampleCount = 0; _numOfChannels = _settingsWidget.numOfChannels(); autoNumOfChannels = (_numOfChannels == NUMOFCHANNELS_AUTO); @@ -139,24 +139,27 @@ void AsciiReader::onDataReady() { numReadChannels = separatedValues.length(); qWarning() << "Incoming data is missing data for some channels!"; + qWarning() << "Read line: " << line; } // parse read line + double* channelSamples = new double[_numOfChannels](); for (unsigned ci = 0; ci < numReadChannels; ci++) { bool ok; - double channelSample = separatedValues[ci].toDouble(&ok); - if (ok) - { - _channelMan->addChannelData(ci, &channelSample, 1); - sampleCount++; - } - else + channelSamples[ci] = separatedValues[ci].toDouble(&ok); + if (!ok) { qWarning() << "Data parsing error for channel: " << ci; + qWarning() << "Read line: " << line; + channelSamples[ci] = 0; } } - emit dataAdded(); + + // 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 @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -30,7 +30,8 @@ class AsciiReader : public AbstractReade Q_OBJECT public: - explicit AsciiReader(QIODevice* device, ChannelManager* channelMan, QObject *parent = 0); + explicit AsciiReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject *parent = 0); QWidget* settingsWidget(); unsigned numOfChannels(); void enable(bool enabled = true); diff --git a/src/binarystreamreader.cpp b/src/binarystreamreader.cpp --- a/src/binarystreamreader.cpp +++ b/src/binarystreamreader.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -23,13 +23,13 @@ #include "binarystreamreader.h" #include "floatswap.h" -BinaryStreamReader::BinaryStreamReader(QIODevice* device, ChannelManager* channelMan, QObject *parent) : - AbstractReader(device, channelMan, parent) +BinaryStreamReader::BinaryStreamReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject* parent) : + AbstractReader(device, channelMan, recorder, parent) { paused = false; skipByteRequested = false; skipSampleRequested = false; - sampleCount = 0; _numOfChannels = _settingsWidget.numOfChannels(); connect(&_settingsWidget, &BinaryStreamReaderSettings::numOfChannelsChanged, @@ -171,15 +171,9 @@ void BinaryStreamReader::onDataReady() } } - for (unsigned int ci = 0; ci < _numOfChannels; ci++) - { - addChannelData(ci, - channelSamples + ci*numOfPackagesToRead, - numOfPackagesToRead); - } - emit dataAdded(); + addData(channelSamples, numOfPackagesToRead*_numOfChannels); - delete channelSamples; + delete[] channelSamples; } template double BinaryStreamReader::readSampleAs() @@ -200,13 +194,6 @@ template double BinaryStream return double(data); } -void BinaryStreamReader::addChannelData(unsigned int channel, - double* data, unsigned size) -{ - _channelMan->addChannelData(channel, data, size); - sampleCount += size; -} - void BinaryStreamReader::saveSettings(QSettings* settings) { _settingsWidget.saveSettings(settings); diff --git a/src/binarystreamreader.h b/src/binarystreamreader.h --- a/src/binarystreamreader.h +++ b/src/binarystreamreader.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -34,7 +34,8 @@ class BinaryStreamReader : public Abstra { Q_OBJECT public: - explicit BinaryStreamReader(QIODevice* device, ChannelManager* channelMan, QObject *parent = 0); + explicit BinaryStreamReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject *parent = 0); QWidget* settingsWidget(); unsigned numOfChannels(); void enable(bool enabled = true); @@ -65,9 +66,6 @@ private: */ template double readSampleAs(); - // `data` contains i th channels data - void addChannelData(unsigned int channel, double* data, unsigned size); - private slots: void onNumberFormatChanged(NumberFormat numberFormat); void onNumOfChannelsChanged(unsigned value); diff --git a/src/channelinfomodel.cpp b/src/channelinfomodel.cpp new file mode 100644 --- /dev/null +++ b/src/channelinfomodel.cpp @@ -0,0 +1,390 @@ +/* + 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 "channelinfomodel.h" +#include "setting_defines.h" + +#define NUMOF_COLORS (32) + +const QColor colors[NUMOF_COLORS] = +{ + QColor("#ff0056"), + QColor("#7e2dd2"), + QColor("#00ae7e"), + QColor("#fe8900"), + QColor("#ff937e"), + QColor("#6a826c"), + QColor("#ff029d"), + QColor("#00b917"), + QColor("#7a4782"), + QColor("#85a900"), + QColor("#a42400"), + QColor("#683d3b"), + QColor("#bdc6ff"), + QColor("#263400"), + QColor("#bdd393"), + QColor("#d5ff00"), + QColor("#9e008e"), + QColor("#001544"), + QColor("#c28c9f"), + QColor("#ff74a3"), + QColor("#01d0ff"), + QColor("#004754"), + QColor("#e56ffe"), + QColor("#788231"), + QColor("#0e4ca1"), + QColor("#91d0cb"), + QColor("#be9970"), + QColor("#968ae8"), + QColor("#bb8800"), + QColor("#43002c"), + QColor("#deff74"), + QColor("#00ffc6") +}; + +ChannelInfoModel::ChannelInfoModel(unsigned numberOfChannels, QObject* parent) : + QAbstractTableModel(parent) +{ + _numOfChannels = 0; + setNumOfChannels(numberOfChannels); +} + +ChannelInfoModel::ChannelInfoModel(const ChannelInfoModel& other) : + ChannelInfoModel(other.rowCount(), other.parent()) +{ + for (int i = 0; i < other.rowCount(); i++) + { + setData(index(i, COLUMN_NAME), + other.data(other.index(i, COLUMN_NAME), Qt::EditRole), + Qt::EditRole); + setData(index(i, COLUMN_NAME), + other.data(other.index(i, COLUMN_NAME), Qt::ForegroundRole), + Qt::ForegroundRole); + setData(index(i, COLUMN_VISIBILITY), + other.data(other.index(i, COLUMN_VISIBILITY), Qt::CheckStateRole), + Qt::CheckStateRole); + } +} + +ChannelInfoModel::ChannelInfoModel(const QStringList& channelNames) : + ChannelInfoModel(channelNames.length(), NULL) +{ + for (int i = 0; i < channelNames.length(); i++) + { + setData(index(i, COLUMN_NAME), channelNames[i], Qt::EditRole); + } +} + +ChannelInfoModel::ChannelInfo::ChannelInfo(unsigned index) +{ + name = tr("Channel %1").arg(index + 1); + visibility = true; + color = colors[index % NUMOF_COLORS]; +} + +QString ChannelInfoModel::name(unsigned i) const +{ + return infos[i].name; +} + +QColor ChannelInfoModel::color(unsigned i) const +{ + return infos[i].color; +} + +bool ChannelInfoModel::isVisible(unsigned i) const +{ + return infos[i].visibility; +} + +QStringList ChannelInfoModel::channelNames() const +{ + QStringList r; + for (unsigned ci = 0; ci < _numOfChannels; ci++) + { + r << name(ci); + } + return r; +} + +int ChannelInfoModel::rowCount(const QModelIndex &parent) const +{ + return _numOfChannels; +} + +int ChannelInfoModel::columnCount(const QModelIndex & parent) const +{ + return COLUMN_COUNT; +} + +Qt::ItemFlags ChannelInfoModel::flags(const QModelIndex &index) const +{ + if (index.column() == COLUMN_NAME) + { + return Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren | Qt::ItemIsSelectable; + } + else if (index.column() == COLUMN_VISIBILITY) + { + return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren | Qt::ItemIsSelectable; + } + + return Qt::NoItemFlags; +} + +QVariant ChannelInfoModel::data(const QModelIndex &index, int role) const +{ + // check index + if (index.row() >= (int) _numOfChannels || index.row() < 0) + { + return QVariant(); + } + + // get color + if (role == Qt::ForegroundRole) + { + return infos[index.row()].color; + } + + // get name + if (index.column() == COLUMN_NAME) + { + if (role == Qt::DisplayRole || role == Qt::EditRole) + { + return QVariant(infos[index.row()].name); + } + } // get visibility + else if (index.column() == COLUMN_VISIBILITY) + { + if (role == Qt::CheckStateRole) + { + bool visible = infos[index.row()].visibility; + return visible ? Qt::Checked : Qt::Unchecked; + } + } + + return QVariant(); +} + +QVariant ChannelInfoModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal) + { + if (role == Qt::DisplayRole) + { + if (section == COLUMN_NAME) + { + return tr("Channel"); + } + else if (section == COLUMN_VISIBILITY) + { + return tr("Visible"); + } + } + } + else // vertical + { + if (section < (int) _numOfChannels && role == Qt::DisplayRole) + { + return QString::number(section + 1); + } + } + + return QVariant(); +} + +bool ChannelInfoModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + // check index + if (index.row() >= (int) _numOfChannels || index.row() < 0) + { + return false; + } + + // set color + if (role == Qt::ForegroundRole) + { + infos[index.row()].color = value.value(); + emit dataChanged(index, index, QVector({Qt::ForegroundRole})); + return true; + } + + // set name + if (index.column() == COLUMN_NAME) + { + if (role == Qt::DisplayRole || role == Qt::EditRole) + { + infos[index.row()].name = value.toString(); + emit dataChanged(index, index, QVector({role})); + return true; + } + } // set visibility + else if (index.column() == COLUMN_VISIBILITY) + { + if (role == Qt::CheckStateRole) + { + bool checked = value.toInt() == Qt::Checked; + infos[index.row()].visibility = checked; + emit dataChanged(index, index, QVector({role})); + return true; + } + } + + // invalid index/role + return false; +} + +void ChannelInfoModel::setNumOfChannels(unsigned number) +{ + if (number == _numOfChannels) return; + + bool isInserting = number > _numOfChannels; + if (isInserting) + { + beginInsertRows(QModelIndex(), _numOfChannels, number-1); + } + else + { + beginRemoveRows(QModelIndex(), number, _numOfChannels-1); + } + + // we create channel info but never remove channel info to + // remember user entered info + if ((int) number > infos.length()) + { + for (unsigned ci = _numOfChannels; ci < number; ci++) + { + infos.append(ChannelInfo(ci)); + } + } + + // make sure newly available channels are visible, we don't + // remember visibility option intentionally so that user doesn't + // get confused + if (number > _numOfChannels) + { + for (unsigned ci = _numOfChannels; ci < number; ci++) + { + infos[ci].visibility = true; + } + } + + _numOfChannels = number; + + if (isInserting) + { + endInsertRows(); + } + else + { + endRemoveRows(); + } +} + +void ChannelInfoModel::resetInfos() +{ + beginResetModel(); + for (unsigned ci = 0; (int) ci < infos.length(); ci++) + { + infos[ci] = ChannelInfo(ci); + } + endResetModel(); +} + +void ChannelInfoModel::resetNames() +{ + beginResetModel(); + for (unsigned ci = 0; (int) ci < infos.length(); ci++) + { + infos[ci].name = ChannelInfo(ci).name; + } + endResetModel(); +} + +void ChannelInfoModel::resetColors() +{ + beginResetModel(); + for (unsigned ci = 0; (int) ci < infos.length(); ci++) + { + infos[ci].color = ChannelInfo(ci).color; + } + endResetModel(); +} + +void ChannelInfoModel::resetVisibility() +{ + beginResetModel(); + for (unsigned ci = 0; (int) ci < infos.length(); ci++) + { + infos[ci].visibility = true; + } + endResetModel(); +} + +void ChannelInfoModel::saveSettings(QSettings* settings) +{ + settings->beginGroup(SettingGroup_Channels); + settings->beginWriteArray(SG_Channels_Channel); + + // save all channel information regardless of current number of channels + for (unsigned ci = 0; (int) ci < infos.length(); ci++) + { + settings->setArrayIndex(ci); + settings->setValue(SG_Channels_Name, infos[ci].name); + settings->setValue(SG_Channels_Color, infos[ci].color); + settings->setValue(SG_Channels_Visible, infos[ci].visibility); + } + + settings->endArray(); + settings->endGroup(); +} + +void ChannelInfoModel::loadSettings(QSettings* settings) +{ + settings->beginGroup(SettingGroup_Channels); + unsigned size = settings->beginReadArray(SG_Channels_Channel); + + for (unsigned ci = 0; ci < size; ci++) + { + settings->setArrayIndex(ci); + + ChannelInfo chanInfo(ci); + chanInfo.name = settings->value(SG_Channels_Name, chanInfo.name).toString(); + chanInfo.color = settings->value(SG_Channels_Color, chanInfo.color).value(); + chanInfo.visibility = settings->value(SG_Channels_Visible, true).toBool(); + + if ((int) ci < infos.size()) + { + infos[ci] = chanInfo; + + if (ci < _numOfChannels) + { + auto roles = QVector({ + Qt::DisplayRole, Qt::EditRole, Qt::ForegroundRole, Qt::CheckStateRole}); + emit dataChanged(index(ci, 0), index(ci, COLUMN_COUNT-1), roles); + } + } + else + { + infos.append(chanInfo); + } + } + + settings->endArray(); + settings->endGroup(); +} diff --git a/src/channelinfomodel.h b/src/channelinfomodel.h new file mode 100644 --- /dev/null +++ b/src/channelinfomodel.h @@ -0,0 +1,93 @@ +/* + 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 CHANNELINFOMODEL_H +#define CHANNELINFOMODEL_H + +#include +#include +#include +#include + +class ChannelInfoModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + enum ChannelInfoColumn + { + COLUMN_NAME = 0, + COLUMN_VISIBILITY, + COLUMN_COUNT + }; + + explicit ChannelInfoModel(unsigned numberOfChannels, QObject *parent = 0); + ChannelInfoModel(const ChannelInfoModel& other); + explicit ChannelInfoModel(const QStringList& channelNames); + + QString name (unsigned i) const; + QColor color (unsigned i) const; + bool isVisible(unsigned i) const; + /// Returns a list of channel names + QStringList channelNames() const; + + // implemented from QAbstractItemModel + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); + Qt::ItemFlags flags(const QModelIndex &index) const; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + + void setNumOfChannels(unsigned number); + /// Stores all channel info into a `QSettings` + void saveSettings(QSettings* settings); + /// Loads all channel info from a `QSettings`. + void loadSettings(QSettings* settings); + +public slots: + /// reset all channel info (names, color etc.) + void resetInfos(); + /// reset all channel names + void resetNames(); + /// reset all channel colors + void resetColors(); + /// reset visibility + void resetVisibility(); + +private: + struct ChannelInfo + { + explicit ChannelInfo(unsigned index); + + QString name; + bool visibility; + QColor color; + }; + + unsigned _numOfChannels; ///< @note this is not necessarily the length of `infos` + + /** + * Channel info is added here but never removed so that we can + * remember user entered info (names, colors etc.). + */ + QList infos; +}; + +#endif // CHANNELINFOMODEL_H diff --git a/src/channelmanager.cpp b/src/channelmanager.cpp --- a/src/channelmanager.cpp +++ b/src/channelmanager.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -17,30 +17,28 @@ along with serialplot. If not, see . */ -#include #include +#include + #include "channelmanager.h" #include "setting_defines.h" ChannelManager::ChannelManager(unsigned numberOfChannels, unsigned numberOfSamples, QObject *parent) : - QObject(parent) + QObject(parent), + _infoModel(numberOfChannels) { _numOfChannels = numberOfChannels; _numOfSamples = numberOfSamples; - - QStringList channelNamesList; + _paused = false; for (unsigned int i = 0; i < numberOfChannels; i++) { channelBuffers.append(new FrameBuffer(numberOfSamples)); - channelNamesList << QString("Channel %1").arg(i+1); } - _channelNames.setStringList(channelNamesList); - - connect(&_channelNames, &QStringListModel::dataChanged, - this, &ChannelManager::onChannelNameDataChange); + connect(&_infoModel, &ChannelInfoModel::dataChanged, + this, &ChannelManager::onChannelInfoChanged); } ChannelManager::~ChannelManager() @@ -71,7 +69,6 @@ void ChannelManager::setNumOfChannels(un for (unsigned int i = 0; i < number - oldNum; i++) { channelBuffers.append(new FrameBuffer(_numOfSamples)); - addChannelName(QString("Channel %1").arg(oldNum+i+1)); } } else if(number < oldNum) @@ -80,10 +77,12 @@ void ChannelManager::setNumOfChannels(un for (unsigned int i = oldNum-1; i > number-1; i--) { delete channelBuffers.takeLast(); - _channelNames.removeRow(i); } } + _numOfChannels = number; + _infoModel.setNumOfChannels(number); + emit numOfChannelsChanged(number); } @@ -99,74 +98,96 @@ void ChannelManager::setNumOfSamples(uns emit numOfSamplesChanged(number); } +void ChannelManager::pause(bool paused) +{ + _paused = paused; +} + FrameBuffer* ChannelManager::channelBuffer(unsigned channel) { return channelBuffers[channel]; } -QStringListModel* ChannelManager::channelNames() +ChannelInfoModel* ChannelManager::infoModel() { - return &_channelNames; + return &_infoModel; } QString ChannelManager::channelName(unsigned channel) { - return _channelNames.data(_channelNames.index(channel, 0), Qt::DisplayRole).toString(); + return _infoModel.data(_infoModel.index(channel, ChannelInfoModel::COLUMN_NAME), + Qt::DisplayRole).toString(); } -void ChannelManager::setChannelName(unsigned channel, QString name) +QStringList ChannelManager::channelNames() { - _channelNames.setData(_channelNames.index(channel, 0), QVariant(name), Qt::DisplayRole); + QStringList list; + for (unsigned ci = 0; ci < _numOfChannels; ci++) + { + list << channelName(ci); + } + return list; } -void ChannelManager::addChannelName(QString name) +void ChannelManager::onChannelInfoChanged(const QModelIndex & topLeft, + const QModelIndex & bottomRight, + const QVector & roles) { - _channelNames.insertRow(_channelNames.rowCount()); - setChannelName(_channelNames.rowCount()-1, name); -} - -void ChannelManager::onChannelNameDataChange(const QModelIndex & topLeft, - const QModelIndex & bottomRight, - const QVector & roles) -{ - Q_UNUSED(roles); int start = topLeft.row(); int end = bottomRight.row(); + int col = topLeft.column(); - // TODO: maybe check `roles` parameter, can't think of a reason for current use case - for (int i = start; i <= end; i++) + for (int ci = start; ci <= end; ci++) { - emit channelNameChanged(i, channelName(i)); + for (auto role : roles) + { + switch (role) + { + case Qt::EditRole: + if (col == ChannelInfoModel::COLUMN_NAME) + { + emit channelNameChanged(ci, channelName(ci)); + } + break; + case Qt::ForegroundRole: + if (col == ChannelInfoModel::COLUMN_NAME) + { + // TODO: emit channel color changed + } + break; + case Qt::CheckStateRole: + if (col == ChannelInfoModel::COLUMN_VISIBILITY) + { + // TODO: emit visibility + } + break; + } + } + // emit channelNameChanged(i, channelName(i)); } } -void ChannelManager::addChannelData(unsigned channel, double* data, unsigned size) +void ChannelManager::addData(double* data, unsigned size) { - channelBuffer(channel)->addSamples(data, size); + Q_ASSERT(size % _numOfChannels == 0); + + if (_paused) return; + + int n = size / _numOfChannels; + for (unsigned ci = 0; ci < _numOfChannels; ci++) + { + channelBuffers[ci]->addSamples(&data[ci*n], n); + } + + emit dataAdded(); } void ChannelManager::saveSettings(QSettings* settings) { - settings->beginGroup(SettingGroup_Channels); - settings->beginWriteArray(SG_Channels_Channel); - for (unsigned i = 0; i < numOfChannels(); i++) - { - settings->setArrayIndex(i); - settings->setValue(SG_Channels_Name, channelName(i)); - } - settings->endArray(); - settings->endGroup(); + _infoModel.saveSettings(settings); } void ChannelManager::loadSettings(QSettings* settings) { - settings->beginGroup(SettingGroup_Channels); - settings->beginReadArray(SG_Channels_Channel); - for (unsigned i = 0; i < numOfChannels(); i++) - { - settings->setArrayIndex(i); - setChannelName(i, settings->value(SG_Channels_Name, channelName(i)).toString()); - } - settings->endArray(); - settings->endGroup(); + _infoModel.loadSettings(settings); } diff --git a/src/channelmanager.h b/src/channelmanager.h --- a/src/channelmanager.h +++ b/src/channelmanager.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -21,12 +21,13 @@ #define CHANNELMANAGER_H #include -#include +#include #include #include #include #include "framebuffer.h" +#include "channelinfomodel.h" class ChannelManager : public QObject { @@ -38,36 +39,55 @@ public: unsigned numOfChannels(); unsigned numOfSamples(); FrameBuffer* channelBuffer(unsigned channel); - QStringListModel* channelNames(); QString channelName(unsigned channel); /// Stores channel names into a `QSettings` void saveSettings(QSettings* settings); /// Loads channel names from a `QSettings`. void loadSettings(QSettings* settings); + /// Returns a model that manages channel information (name, color etc) + ChannelInfoModel* infoModel(); + /// Returns a list of channel names + QStringList channelNames(); signals: void numOfChannelsChanged(unsigned value); void numOfSamplesChanged(unsigned value); void channelNameChanged(unsigned channel, QString name); + void dataAdded(); ///< emitted when data added to channel man. public slots: void setNumOfChannels(unsigned number); void setNumOfSamples(unsigned number); - void setChannelName(unsigned channel, QString name); - void addChannelData(unsigned channel, double* data, unsigned size); + /** + * Add data for all channels. + * + * All channels data is provided in a single array which contains equal + * number of samples for all channels. Structure is as shown below: + * + * [CH0_SMP0, CH0_SMP1 ... CH0_SMPN, CH1_SMP0, CH1_SMP1, ... , CHN_SMPN] + * + * @param data samples for all channels + * @param size size of `data`, must be multiple of `numOfChannels` + */ + void addData(double* data, unsigned size); + + /// When paused `addData` does nothing. + void pause(bool paused); private: unsigned _numOfChannels; unsigned _numOfSamples; + bool _paused; QList channelBuffers; - QStringListModel _channelNames; + // QStringListModel _channelNames; + ChannelInfoModel _infoModel; void addChannelName(QString name); ///< appends a new channel name at the end of list private slots: - void onChannelNameDataChange(const QModelIndex & topLeft, - const QModelIndex & bottomRight, - const QVector & roles = QVector ()); + void onChannelInfoChanged(const QModelIndex & topLeft, + const QModelIndex & bottomRight, + const QVector & roles = QVector ()); }; #endif // CHANNELMANAGER_H diff --git a/src/dataformatpanel.cpp b/src/dataformatpanel.cpp --- a/src/dataformatpanel.cpp +++ b/src/dataformatpanel.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -31,26 +31,27 @@ DataFormatPanel::DataFormatPanel(QSerialPort* port, ChannelManager* channelMan, + DataRecorder* recorder, QWidget *parent) : QWidget(parent), ui(new Ui::DataFormatPanel), - bsReader(port, channelMan, this), - asciiReader(port, channelMan, this), - framedReader(port, channelMan, this), - demoReader(port, channelMan, this) + bsReader(port, channelMan, recorder, this), + asciiReader(port, channelMan, recorder, this), + framedReader(port, channelMan, recorder, this), + demoReader(port, channelMan, recorder, this) { ui->setupUi(this); serialPort = port; _channelMan = channelMan; paused = false; + demoEnabled = false; // initalize default reader currentReader = &bsReader; bsReader.enable(); ui->rbBinary->setChecked(true); ui->horizontalLayout->addWidget(bsReader.settingsWidget(), 1); - connect(&bsReader, SIGNAL(dataAdded()), this, SIGNAL(dataAdded())); connect(&bsReader, SIGNAL(numOfChannelsChanged(unsigned)), this, SIGNAL(numOfChannelsChanged(unsigned))); connect(&bsReader, SIGNAL(samplesPerSecondChanged(unsigned)), @@ -99,8 +100,7 @@ void DataFormatPanel::enableDemo(bool en if (enabled) { demoReader.enable(); - connect(&demoReader, &DemoReader::dataAdded, - this, &DataFormatPanel::dataAdded); + demoReader.recording = currentReader->recording; connect(&demoReader, &DemoReader::samplesPerSecondChanged, this, &DataFormatPanel::samplesPerSecondChanged); } @@ -109,12 +109,19 @@ void DataFormatPanel::enableDemo(bool en demoReader.enable(false); disconnect(&demoReader, 0, this, 0); } + demoEnabled = enabled; } -void DataFormatPanel::addChannelData(unsigned int channel, - double* data, unsigned size) +void DataFormatPanel::startRecording() { - _channelMan->addChannelData(channel, data, size); + currentReader->recording = true; + if (demoEnabled) demoReader.recording = true; +} + +void DataFormatPanel::stopRecording() +{ + currentReader->recording = false; + if (demoEnabled) demoReader.recording = false; } void DataFormatPanel::selectReader(AbstractReader* reader) @@ -124,7 +131,6 @@ void DataFormatPanel::selectReader(Abstr // re-connect signals disconnect(currentReader, 0, this, 0); - connect(reader, SIGNAL(dataAdded()), this, SIGNAL(dataAdded())); connect(reader, SIGNAL(numOfChannelsChanged(unsigned)), this, SIGNAL(numOfChannelsChanged(unsigned))); connect(reader, SIGNAL(samplesPerSecondChanged(unsigned)), @@ -142,8 +148,8 @@ void DataFormatPanel::selectReader(Abstr emit numOfChannelsChanged(reader->numOfChannels()); } - // pause reader->pause(paused); + reader->recording = currentReader->recording; currentReader = reader; } diff --git a/src/dataformatpanel.h b/src/dataformatpanel.h --- a/src/dataformatpanel.h +++ b/src/dataformatpanel.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -34,6 +34,7 @@ #include "asciireader.h" #include "demoreader.h" #include "framedreader.h" +#include "datarecorder.h" namespace Ui { class DataFormatPanel; @@ -46,7 +47,8 @@ class DataFormatPanel : public QWidget public: explicit DataFormatPanel(QSerialPort* port, ChannelManager* channelMan, - QWidget *parent = 0); + DataRecorder* recorder, + QWidget* parent = 0); ~DataFormatPanel(); /// Returns currently selected number of channels @@ -60,10 +62,19 @@ public slots: void pause(bool); void enableDemo(bool); // demo shouldn't be enabled when port is open + /** + * @brief Starts sending data to recorder. + * + * @note recorder must have been started! + */ + void startRecording(); + + /// Stops recording. + void stopRecording(); + signals: void numOfChannelsChanged(unsigned); void samplesPerSecondChanged(unsigned); - void dataAdded(); private: Ui::DataFormatPanel *ui; @@ -81,10 +92,8 @@ private: bool paused; + bool demoEnabled; DemoReader demoReader; - - // `data` contains i th channels data - void addChannelData(unsigned int channel, double* data, unsigned size); }; #endif // DATAFORMATPANEL_H diff --git a/src/datarecorder.cpp b/src/datarecorder.cpp new file mode 100644 --- /dev/null +++ b/src/datarecorder.cpp @@ -0,0 +1,95 @@ +/* + 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 "datarecorder.h" + +#include + +DataRecorder::DataRecorder(QObject *parent) : + QObject(parent), + fileStream(&file) +{ + lastNumChannels = 0; + disableBuffering = false; + windowsLE = false; +} + +bool DataRecorder::startRecording(QString fileName, QString separator, QStringList channelNames) +{ + Q_ASSERT(!file.isOpen()); + _sep = separator; + + // open file + file.setFileName(fileName); + if (!file.open(QIODevice::WriteOnly)) + { + qCritical() << "Opening file " << fileName + << " for recording failed with error: " << file.error(); + return false; + } + + // write header line + if (!channelNames.isEmpty()) + { + fileStream << channelNames.join(_sep); + fileStream << le(); + lastNumChannels = channelNames.length(); + } + return true; +} + +void DataRecorder::addData(double* data, unsigned length, unsigned numOfChannels) +{ + Q_ASSERT(length > 0); + Q_ASSERT(length % numOfChannels == 0); + + if (lastNumChannels != 0 && numOfChannels != lastNumChannels) + { + qWarning() << "Number of channels changed from " << lastNumChannels + << " to " << numOfChannels << + " during recording, CSV file is corrupted but no data will be lost."; + } + lastNumChannels = numOfChannels; + + unsigned numOfSamples = length / numOfChannels; // per channel + for (unsigned int i = 0; i < numOfSamples; i++) + { + for (unsigned ci = 0; ci < numOfChannels; ci++) + { + fileStream << data[ci * numOfSamples + i]; + if (ci != numOfChannels-1) fileStream << _sep; + } + fileStream << le(); + } + + if (disableBuffering) fileStream.flush(); +} + +void DataRecorder::stopRecording() +{ + Q_ASSERT(file.isOpen()); + + file.close(); + lastNumChannels = 0; +} + +const char* DataRecorder::le() const +{ + return windowsLE ? "\r\n" : "\n"; +} diff --git a/src/datarecorder.h b/src/datarecorder.h new file mode 100644 --- /dev/null +++ b/src/datarecorder.h @@ -0,0 +1,88 @@ +/* + 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 DATARECORDER_H +#define DATARECORDER_H + +#include +#include +#include + +class DataRecorder : public QObject +{ + Q_OBJECT +public: + explicit DataRecorder(QObject *parent = 0); + + /// Disables file buffering + 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. + * + * @param fileName name of the recording file + * @param separator column separator + * @param channelNames names of the channels for header line, if empty no header line is written + * @return false if file operation fails (read only etc.) + */ + bool startRecording(QString fileName, QString separator, QStringList channelNames); + + /** + * @brief Adds data to a channel. + * + * Multiple rows of data can be added at a time. Each channels + * data should be ordered consecutively in the `data` array: + * + * [CH0_SMP0, CH0_SMP1 ... CH0_SMPN, CH1_SMP0, CH1_SMP1, ... , CHN_SMPN] + * + * If `numOfChannels` changes during recording, no data will be + * lost (ie. it will be written to the file) but this will produce + * an invalid CSV file. An error message will be written to the + * console. + * + * @param data samples array + * @param length number of samples in `data`, must be multiple of `numOfChannels` + * @param numOfChannels how many channels samples this data carries + */ + void addData(double* data, unsigned length, unsigned numOfChannels); + + /// Stops recording, closes file. + void stopRecording(); + +private: + unsigned lastNumChannels; ///< used for error message only + QFile file; + QTextStream fileStream; + QString _sep; + + /// Returns the selected line ending. + const char* le() const; +}; + +#endif // DATARECORDER_H diff --git a/src/demoreader.cpp b/src/demoreader.cpp --- a/src/demoreader.cpp +++ b/src/demoreader.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -25,8 +25,9 @@ #define M_PI 3.14159265358979323846 #endif -DemoReader::DemoReader(QIODevice* device, ChannelManager* channelMan, QObject *parent) : - AbstractReader(device, channelMan, parent) +DemoReader::DemoReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject* parent) : + AbstractReader(device, channelMan, recorder, parent) { paused = false; _numOfChannels = 1; @@ -76,13 +77,13 @@ void DemoReader::demoTimerTimeout() if (!paused) { + double* samples = new double[_numOfChannels]; for (unsigned ci = 0; ci < _numOfChannels; ci++) { // we are calculating the fourier components of square wave - double value = 4*sin(2*M_PI*double((ci+1)*count)/period)/((2*(ci+1))*M_PI); - _channelMan->addChannelData(ci, &value, 1); - sampleCount++; + samples[ci] = 4*sin(2*M_PI*double((ci+1)*count)/period)/((2*(ci+1))*M_PI); } - emit dataAdded(); + addData(samples, _numOfChannels); + delete[] samples; } } diff --git a/src/demoreader.h b/src/demoreader.h --- a/src/demoreader.h +++ b/src/demoreader.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -38,7 +38,8 @@ class DemoReader : public AbstractReader Q_OBJECT public: - explicit DemoReader(QIODevice* device, ChannelManager* channelMan, QObject *parent = 0); + explicit DemoReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject* parent = 0); /// Demo reader is an exception so this function returns NULL QWidget* settingsWidget(); diff --git a/src/framebuffer.cpp b/src/framebuffer.cpp --- a/src/framebuffer.cpp +++ b/src/framebuffer.cpp @@ -30,7 +30,7 @@ FrameBuffer::FrameBuffer(size_t size) FrameBuffer::~FrameBuffer() { - delete data; + delete[] data; } void FrameBuffer::resize(size_t size) 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. @@ -21,9 +21,19 @@ FrameBufferSeries::FrameBufferSeries(FrameBuffer* buffer) { + xAsIndex = true; + _xmin = 0; + _xmax = 1; _buffer = buffer; } +void FrameBufferSeries::setXAxis(bool asIndex, double xmin, double xmax) +{ + xAsIndex = asIndex; + _xmin = xmin; + _xmax = xmax; +} + size_t FrameBufferSeries::size() const { return _buffer->size(); @@ -31,10 +41,27 @@ size_t FrameBufferSeries::size() const QPointF FrameBufferSeries::sample(size_t i) const { - return QPointF(i, _buffer->sample(i)); + if (xAsIndex) + { + return QPointF(i, _buffer->sample(i)); + } + else + { + return QPointF(i * (_xmax - _xmin) / 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; + } } 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,6 +37,9 @@ 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; @@ -44,6 +47,9 @@ public: private: FrameBuffer* _buffer; + bool xAsIndex; + double _xmin; + double _xmax; }; #endif // FRAMEBUFFERSERIES_H diff --git a/src/framedreader.cpp b/src/framedreader.cpp --- a/src/framedreader.cpp +++ b/src/framedreader.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -23,8 +23,9 @@ #include "framedreader.h" -FramedReader::FramedReader(QIODevice* device, ChannelManager* channelMan, QObject *parent) : - AbstractReader(device, channelMan, parent) +FramedReader::FramedReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject* parent) : + AbstractReader(device, channelMan, recorder, parent) { paused = false; @@ -310,22 +311,14 @@ void FramedReader::readFrameDataAndCheck if (!checksumEnabled || checksumPassed) { // commit data - for (unsigned int ci = 0; ci < _numOfChannels; ci++) - { - _channelMan->addChannelData( - ci, - channelSamples + ci*numOfPackagesToRead, - numOfPackagesToRead); - sampleCount += numOfPackagesToRead; - } - emit dataAdded(); + addData(channelSamples, numOfPackagesToRead*_numOfChannels); } else { qCritical() << "Checksum failed! Received:" << rChecksum << "Calculated:" << calcChecksum; } - delete channelSamples; + delete[] channelSamples; } template double FramedReader::readSampleAs() diff --git a/src/framedreader.h b/src/framedreader.h --- a/src/framedreader.h +++ b/src/framedreader.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -33,7 +33,8 @@ class FramedReader : public AbstractRead Q_OBJECT public: - explicit FramedReader(QIODevice* device, ChannelManager* channelMan, QObject *parent = 0); + explicit FramedReader(QIODevice* device, ChannelManager* channelMan, + DataRecorder* recorder, QObject *parent = 0); QWidget* settingsWidget(); unsigned numOfChannels(); void enable(bool enabled = true); @@ -83,8 +84,6 @@ private: /// reads payload portion of the frame, calculates checksum and commits data /// @note should be called only if there are enough bytes on device void readFrameDataAndCheck(); - // `data` contains i th channels data - void addChannelData(unsigned int channel, double* data, unsigned size); private slots: void onDataReady(); 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/main.cpp b/src/main.cpp --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2015 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -24,7 +24,6 @@ #include "tooltipfilter.h" #include "version.h" - MainWindow* pMainWindow; void messageHandler(QtMsgType type, const QMessageLogContext &context, @@ -50,5 +49,6 @@ int main(int argc, char *argv[]) qDebug() << "Revision" << VERSION_REVISION; w.show(); + return a.exec(); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -62,19 +62,22 @@ MainWindow::MainWindow(QWidget *parent) channelMan(1, 1, this), snapshotMan(this, &channelMan), commandPanel(&serialPort), - dataFormatPanel(&serialPort, &channelMan) + dataFormatPanel(&serialPort, &channelMan, &recorder), + recordPanel(&recorder, &channelMan) { ui->setupUi(this); - plotMan = new PlotManager(ui->plotArea); + plotMan = new PlotManager(ui->plotArea, channelMan.infoModel()); ui->tabWidget->insertTab(0, &portControl, "Port"); ui->tabWidget->insertTab(1, &dataFormatPanel, "Data Format"); ui->tabWidget->insertTab(2, &plotControlPanel, "Plot"); ui->tabWidget->insertTab(3, &commandPanel, "Commands"); + ui->tabWidget->insertTab(4, &recordPanel, "Record"); ui->tabWidget->setCurrentIndex(0); auto tbPortControl = portControl.toolBar(); addToolBar(tbPortControl); + addToolBar(recordPanel.toolbar()); ui->plotToolBar->addAction(snapshotMan.takeSnapshotAction()); ui->menuBar->insertMenu(ui->menuHelp->menuAction(), snapshotMan.menu()); @@ -83,6 +86,7 @@ MainWindow::MainWindow(QWidget *parent) connect(&commandPanel, &CommandPanel::focusRequested, [this]() { this->ui->tabWidget->setCurrentWidget(&commandPanel); + this->ui->tabWidget->showTabs(); }); tbPortControl->setObjectName("tbPortControl"); @@ -130,14 +134,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); - 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())); @@ -150,11 +158,42 @@ MainWindow::MainWindow(QWidget *parent) this, SLOT(onPortError(QSerialPort::SerialPortError))); // init data format and reader - QObject::connect(&dataFormatPanel, &DataFormatPanel::dataAdded, + QObject::connect(&channelMan, &ChannelManager::dataAdded, plotMan, &PlotManager::replot); QObject::connect(ui->actionPause, &QAction::triggered, - &dataFormatPanel, &DataFormatPanel::pause); + &channelMan, &ChannelManager::pause); + + QObject::connect(&recordPanel, &RecordPanel::recordStarted, + &dataFormatPanel, &DataFormatPanel::startRecording); + + QObject::connect(&recordPanel, &RecordPanel::recordStopped, + &dataFormatPanel, &DataFormatPanel::stopRecording); + + QObject::connect(ui->actionPause, &QAction::triggered, + [this](bool enabled) + { + if (enabled && !recordPanel.recordPaused()) + { + dataFormatPanel.pause(true); + } + else + { + dataFormatPanel.pause(false); + } + }); + + QObject::connect(&recordPanel, &RecordPanel::recordPausedChanged, + [this](bool enabled) + { + if (ui->actionPause->isChecked() && enabled) + { + dataFormatPanel.pause(false); + } + }); + + connect(&serialPort, &QIODevice::aboutToClose, + &recordPanel, &RecordPanel::onPortClose); // init data arrays and plot numOfSamples = plotControlPanel.numOfSamples(); @@ -169,10 +208,7 @@ MainWindow::MainWindow(QWidget *parent) connect(&channelMan, &ChannelManager::numOfChannelsChanged, this, &MainWindow::onNumOfChannelsChanged); - connect(&channelMan, &ChannelManager::channelNameChanged, - this, &MainWindow::onChannelNameChanged); - - plotControlPanel.setChannelNamesModel(channelMan.channelNames()); + plotControlPanel.setChannelInfoModel(channelMan.infoModel()); // init curve list for (unsigned int i = 0; i < numOfChannels; i++) @@ -180,9 +216,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->onNumOfSamplesChanged(numOfSamples); // Init sps (sample per second) counter spsLabel.setText("0sps"); @@ -215,15 +254,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(); @@ -237,6 +273,7 @@ MainWindow::~MainWindow() void MainWindow::closeEvent(QCloseEvent * event) { + // save snapshots if (!snapshotMan.isAllSaved()) { auto clickedButton = QMessageBox::warning( @@ -250,6 +287,41 @@ void MainWindow::closeEvent(QCloseEvent 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); } @@ -369,17 +441,6 @@ void MainWindow::onNumOfChannelsChanged( plotMan->replot(); } -void MainWindow::onChannelNameChanged(unsigned channel, QString name) -{ - // This slot is triggered also when a new channel is added, in - // this case curve list doesn't contain said channel. No worries, - // since `onNumOfChannelsChanged` slot will update curve list. - if (channel < plotMan->numOfCurves()) // check if channel exists in curve list - { - plotMan->setTitle(channel, name); - } -} - void MainWindow::onSpsChanged(unsigned sps) { spsLabel.setText(QString::number(sps) + "sps"); @@ -463,6 +524,11 @@ void MainWindow::messageHandler(QtMsgTyp { ui->statusBar->showMessage(msg, 5000); } + + if (type == QtFatalMsg) + { + __builtin_trap(); + } } void MainWindow::saveAllSettings(QSettings* settings) @@ -474,6 +540,7 @@ void MainWindow::saveAllSettings(QSettin plotControlPanel.saveSettings(settings); plotMan->saveSettings(settings); commandPanel.saveSettings(settings); + recordPanel.saveSettings(settings); } void MainWindow::loadAllSettings(QSettings* settings) @@ -485,6 +552,7 @@ void MainWindow::loadAllSettings(QSettin plotControlPanel.loadSettings(settings); plotMan->loadSettings(settings); commandPanel.loadSettings(settings); + recordPanel.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 @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -38,11 +38,13 @@ #include "commandpanel.h" #include "dataformatpanel.h" #include "plotcontrolpanel.h" +#include "recordpanel.h" #include "ui_about_dialog.h" #include "framebuffer.h" #include "channelmanager.h" #include "snapshotmanager.h" #include "plotmanager.h" +#include "datarecorder.h" namespace Ui { class MainWindow; @@ -74,10 +76,12 @@ private: ChannelManager channelMan; PlotManager* plotMan; SnapshotManager snapshotMan; + DataRecorder recorder; // operated by `recordPanel` QLabel spsLabel; CommandPanel commandPanel; DataFormatPanel dataFormatPanel; + RecordPanel recordPanel; PlotControlPanel plotControlPanel; bool isDemoRunning(); @@ -99,7 +103,6 @@ private slots: void onNumOfSamplesChanged(int value); void onNumOfChannelsChanged(unsigned value); - void onChannelNameChanged(unsigned channel, QString name); void clearPlot(); void onSpsChanged(unsigned sps); diff --git a/src/mainwindow.ui b/src/mainwindow.ui --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -141,6 +141,9 @@ false + + + Pause @@ -152,6 +155,9 @@ + + + Clear 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,7 @@ Plot::Plot(QWidget* parent) : { isAutoScaled = true; symbolSize = 0; + numOfSamples = 1; QObject::connect(&zoomer, &Zoomer::unzoomed, this, &Plot::unzoomed); @@ -78,7 +79,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 +93,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 +125,13 @@ void Plot::resetAxes() setAxisScale(QwtPlot::yLeft, yMin, yMax); } + zoomer.setZoomBase(); + replot(); } void Plot::unzoomed() { - setAxisAutoScale(QwtPlot::xBottom); resetAxes(); onXScaleChanged(); } @@ -229,7 +252,10 @@ void Plot::onXScaleChanged() 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) { @@ -273,8 +299,6 @@ void Plot::resizeEvent(QResizeEvent * ev void Plot::onNumOfSamplesChanged(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. @@ -37,28 +37,14 @@ class Plot : public QwtPlot { Q_OBJECT + friend class PlotManager; + public: Plot(QWidget* parent = 0); ~Plot(); static QColor makeColor(unsigned int channelIndex); -private: - bool isAutoScaled; - double yMin, yMax; - int symbolSize; - Zoomer zoomer; - ScaleZoomer sZoomer; - QwtPlotGrid grid; - PlotSnapshotOverlay* snapshotOverlay; - QwtPlotLegendItem legend; - QwtPlotTextLabel demoIndicator; - - /// update the display of symbols depending on `symbolSize` - void updateSymbols(); - void resetAxes(); - void resizeEvent(QResizeEvent * event); - public slots: void showGrid(bool show = true); void showMinorGrid(bool show = true); @@ -66,7 +52,8 @@ public slots: void showDemoIndicator(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); /** * Displays an animation for snapshot. @@ -77,6 +64,26 @@ public slots: void onNumOfSamplesChanged(unsigned value); +protected: + /// update the display of symbols depending on `symbolSize` + void updateSymbols(); + +private: + bool isAutoScaled; + double yMin, yMax; + double _xMin, _xMax; + unsigned numOfSamples; + int symbolSize; + Zoomer zoomer; + ScaleZoomer sZoomer; + QwtPlotGrid grid; + PlotSnapshotOverlay* snapshotOverlay; + QwtPlotLegendItem legend; + QwtPlotTextLabel demoIndicator; + + void resetAxes(); + void resizeEvent(QResizeEvent * event); + private slots: void unzoomed(); void onXScaleChanged(); diff --git a/src/plotcontrolpanel.cpp b/src/plotcontrolpanel.cpp --- a/src/plotcontrolpanel.cpp +++ b/src/plotcontrolpanel.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -23,6 +23,7 @@ #include +#include "color_selector.hpp" #include "plotcontrolpanel.h" #include "ui_plotcontrolpanel.h" #include "setting_defines.h" @@ -41,7 +42,12 @@ Q_DECLARE_METATYPE(Range); PlotControlPanel::PlotControlPanel(QWidget *parent) : QWidget(parent), - ui(new Ui::PlotControlPanel) + ui(new Ui::PlotControlPanel), + resetAct(tr("Reset"), this), + resetNamesAct(tr("Reset Names"), this), + resetColorsAct(tr("Reset Colors"), this), + showAllAct(tr("Show All"), this), + resetMenu(tr("Reset Menu"), this) { ui->setupUi(this); @@ -55,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))); @@ -68,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 { @@ -92,6 +113,18 @@ PlotControlPanel::PlotControlPanel(QWidg QObject::connect(ui->cbRangePresets, SIGNAL(activated(int)), this, SLOT(onRangeSelected())); + + // color selector starts disabled until a channel is selected + ui->colorSelector->setColor(QColor(0,0,0,0)); + ui->colorSelector->setDisplayMode(color_widgets::ColorPreview::AllAlpha); + ui->colorSelector->setDisabled(true); + + // reset button + resetMenu.addAction(&resetNamesAct); + resetMenu.addAction(&resetColorsAct); + resetMenu.addAction(&showAllAct); + resetAct.setMenu(&resetMenu); + ui->tbReset->setDefaultAction(&resetAct); } PlotControlPanel::~PlotControlPanel() @@ -163,7 +196,7 @@ void PlotControlPanel::onAutoScaleChecke ui->spYmin->setEnabled(false); ui->spYmax->setEnabled(false); - emit scaleChanged(true); // autoscale + emit yScaleChanged(true); // autoscale } else { @@ -172,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(); @@ -204,15 +255,116 @@ void PlotControlPanel::onRangeSelected() ui->cbAutoScale->setChecked(false); } -void PlotControlPanel::setChannelNamesModel(QAbstractItemModel * model) +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->lvChannelNames->setModel(model); + ui->tvChannelInfo->setModel(model); + + // channel color selector + connect(ui->tvChannelInfo->selectionModel(), &QItemSelectionModel::currentRowChanged, + [this](const QModelIndex ¤t, const QModelIndex &previous) + { + // TODO: duplicate with below lambda + QColor color(0,0,0,0); // transparent + + if (current.isValid()) + { + ui->colorSelector->setEnabled(true); + auto model = ui->tvChannelInfo->model(); + color = model->data(current, Qt::ForegroundRole).value(); + } + else + { + ui->colorSelector->setDisabled(true); + } + + // temporarily block signals because `setColor` emits `colorChanged` + bool wasBlocked = ui->colorSelector->blockSignals(true); + ui->colorSelector->setColor(color); + ui->colorSelector->blockSignals(wasBlocked); + }); + + connect(ui->tvChannelInfo->selectionModel(), &QItemSelectionModel::selectionChanged, + [this](const QItemSelection & selected, const QItemSelection & deselected) + { + if (!selected.length()) + { + ui->colorSelector->setDisabled(true); + + // temporarily block signals because `setColor` emits `colorChanged` + bool wasBlocked = ui->colorSelector->blockSignals(true); + ui->colorSelector->setColor(QColor(0,0,0,0)); + ui->colorSelector->blockSignals(wasBlocked); + } + }); + + connect(ui->colorSelector, &color_widgets::ColorSelector::colorChanged, + [this](QColor color) + { + auto index = ui->tvChannelInfo->selectionModel()->currentIndex(); + ui->tvChannelInfo->model()->setData(index, color, Qt::ForegroundRole); + }); + + connect(model, &QAbstractItemModel::dataChanged, + [this](const QModelIndex & topLeft, const QModelIndex & bottomRight, const QVector & roles = QVector ()) + { + auto current = ui->tvChannelInfo->selectionModel()->currentIndex(); + + // no current selection + if (!current.isValid()) return; + + auto mod = ui->tvChannelInfo->model(); + QColor color = mod->data(current, Qt::ForegroundRole).value(); + + // temporarily block signals because `setColor` emits `colorChanged` + bool wasBlocked = ui->colorSelector->blockSignals(true); + ui->colorSelector->setColor(color); + ui->colorSelector->blockSignals(wasBlocked); + }); + + // reset actions + connect(&resetAct, &QAction::triggered, model, &ChannelInfoModel::resetInfos); + connect(&resetNamesAct, &QAction::triggered, model, &ChannelInfoModel::resetNames); + connect(&resetColorsAct, &QAction::triggered, model, &ChannelInfoModel::resetColors); + connect(&showAllAct, &QAction::triggered, model, &ChannelInfoModel::resetVisibility); } void PlotControlPanel::saveSettings(QSettings* settings) { 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()); @@ -224,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 @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -21,8 +21,11 @@ #define PLOTCONTROLPANEL_H #include -#include #include +#include +#include + +#include "channelinfomodel.h" namespace Ui { class PlotControlPanel; @@ -37,11 +40,14 @@ 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 setChannelNamesModel(QAbstractItemModel * model); + void setChannelInfoModel(ChannelInfoModel* model); /// Stores plot settings into a `QSettings` void saveSettings(QSettings* settings); @@ -50,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; @@ -60,6 +67,9 @@ private: /// User can disable this setting in the checkbox bool warnNumOfSamples; + QAction resetAct, resetNamesAct, resetColorsAct, showAllAct; + QMenu resetMenu; + /// Show a confirmation dialog before setting #samples to a big value bool askNSConfirmation(int value); @@ -68,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 @@ -15,28 +15,108 @@ - - - - - font-weight: bold; - - - Channel Names: - - - - - - - - 16777215 - 170 - - - - - + + + + 0 + 0 + + + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 300 + 170 + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + + + + QLayout::SetMaximumSize + + + + + + 0 + 0 + + + + + 20 + 20 + + + + + 20 + 20 + + + + + + + + Qt::Horizontal + + + + 1 + 20 + + + + + + + + Reset + + + QToolButton::MenuButtonPopup + + + Qt::NoArrow + + + + + + + @@ -48,7 +128,7 @@ - QFormLayout::AllNonFixedFieldsGrow + QFormLayout::FieldsStayAtSizeHint @@ -77,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 @@ -86,81 +247,115 @@ - - - - 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: - + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 1 + 20 + + + + + + + color_widgets::ColorSelector + QWidget +
color_selector.hpp
+ 1 +
+
diff --git a/src/plotmanager.cpp b/src/plotmanager.cpp --- a/src/plotmanager.cpp +++ b/src/plotmanager.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -17,14 +17,14 @@ along with serialplot. If not, see . */ -#include +#include "qwt_symbol.h" #include "plot.h" #include "plotmanager.h" #include "utils.h" #include "setting_defines.h" -PlotManager::PlotManager(QWidget* plotArea, QObject *parent) : +PlotManager::PlotManager(QWidget* plotArea, ChannelInfoModel* infoModel, QObject *parent) : QObject(parent), _plotArea(plotArea), showGridAction("&Grid", this), @@ -38,6 +38,8 @@ PlotManager::PlotManager(QWidget* plotAr _yMin = 0; _yMax = 1; isDemoShown = false; + _infoModel = infoModel; + _numOfSamples = 1; // initalize layout and single widget isMulti = false; @@ -85,6 +87,21 @@ PlotManager::PlotManager(QWidget* plotAr this, &PlotManager::showLegend); connect(&showMultiAction, SELECT::OVERLOAD_OF(&QAction::triggered), this, &PlotManager::setMulti); + + // connect to channel info model + if (_infoModel != NULL) // TODO: remove when snapshots have infomodel + { + connect(_infoModel, &QAbstractItemModel::dataChanged, + this, &PlotManager::onChannelInfoChanged); + + connect(_infoModel, &QAbstractItemModel::modelReset, + [this]() + { + onChannelInfoChanged(_infoModel->index(0, 0), // start + _infoModel->index(_infoModel->rowCount()-1, 0), // end + {}); // roles ignored + }); + } } PlotManager::~PlotManager() @@ -103,6 +120,46 @@ PlotManager::~PlotManager() if (scrollArea != NULL) delete scrollArea; } +void PlotManager::onChannelInfoChanged(const QModelIndex &topLeft, + const QModelIndex &bottomRight, + const QVector &roles) +{ + int start = topLeft.row(); + int end = bottomRight.row(); + + for (int ci = start; ci <= end; ci++) + { + QString name = topLeft.sibling(ci, ChannelInfoModel::COLUMN_NAME).data(Qt::EditRole).toString(); + QColor color = topLeft.sibling(ci, ChannelInfoModel::COLUMN_NAME).data(Qt::ForegroundRole).value(); + bool visible = topLeft.sibling(ci, ChannelInfoModel::COLUMN_VISIBILITY).data(Qt::CheckStateRole).toBool(); + + curves[ci]->setTitle(name); + curves[ci]->setPen(color); + curves[ci]->setVisible(visible); + curves[ci]->setItemAttribute(QwtPlotItem::Legend, visible); + + // replot only updated widgets + if (isMulti) + { + plotWidgets[ci]->updateSymbols(); // required for color change + plotWidgets[ci]->updateLegend(curves[ci]); + plotWidgets[ci]->setVisible(visible); + if (visible) + { + plotWidgets[ci]->replot(); + } + } + } + + // replot single widget + if (!isMulti) + { + plotWidgets[0]->updateSymbols(); + plotWidgets[0]->updateLegend(); + replot(); + } +} + void PlotManager::setMulti(bool enabled) { if (enabled == isMulti) return; @@ -194,7 +251,16 @@ Plot* PlotManager::addPlotWidget() plot->showMinorGrid(showMinorGridAction.isChecked()); plot->showLegend(showLegendAction.isChecked()); plot->showDemoIndicator(isDemoShown); - plot->setAxis(_autoScaled, _yMin, _yMax); + plot->setYAxis(_autoScaled, _yMin, _yMax); + + if (_xAxisAsIndex) + { + plot->setXAxis(0, _numOfSamples); + } + else + { + plot->setXAxis(_xMin, _xMax); + } return plot; } @@ -202,7 +268,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); } @@ -219,7 +287,8 @@ void PlotManager::_addCurve(QwtPlotCurve curves.append(curve); unsigned index = curves.size()-1; - curve->setPen(Plot::makeColor(index)); + auto color = _infoModel->color(index); + curve->setPen(color); // create the plot for the curve if we are on multi display Plot* plot; @@ -346,17 +415,42 @@ void PlotManager::darkBackground(bool en } } -void PlotManager::setAxis(bool autoScaled, double yAxisMin, double yAxisMax) +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) @@ -367,9 +461,11 @@ void PlotManager::flashSnapshotOverlay() void PlotManager::onNumOfSamplesChanged(unsigned value) { + _numOfSamples = value; for (auto plot : plotWidgets) { plot->onNumOfSamplesChanged(value); + if (_xAxisAsIndex) plot->setXAxis(0, value); } } diff --git a/src/plotmanager.h b/src/plotmanager.h --- a/src/plotmanager.h +++ b/src/plotmanager.h @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -30,13 +30,14 @@ #include #include "plot.h" #include "framebufferseries.h" +#include "channelinfomodel.h" class PlotManager : public QObject { Q_OBJECT public: - explicit PlotManager(QWidget* plotArea, QObject *parent = 0); + explicit PlotManager(QWidget* plotArea, ChannelInfoModel* infoModel = NULL, QObject *parent = 0); ~PlotManager(); /// Add a new curve with title and buffer. A color is /// automatically chosen for curve. @@ -64,7 +65,9 @@ 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 @@ -77,10 +80,15 @@ private: QScrollArea* scrollArea; QList curves; QList plotWidgets; + ChannelInfoModel* _infoModel; bool isDemoShown; bool _autoScaled; double _yMin; double _yMax; + bool _xAxisAsIndex; + double _xMin; + double _xMax; + unsigned _numOfSamples; // menu actions QAction showGridAction; @@ -104,6 +112,10 @@ private slots: void showLegend(bool show = true); void unzoom(); void darkBackground(bool enabled = true); + + void onChannelInfoChanged(const QModelIndex & topLeft, + const QModelIndex & bottomRight, + const QVector & roles = QVector ()); }; #endif // PLOTMANAGER_H diff --git a/src/portcontrol.ui b/src/portcontrol.ui --- a/src/portcontrol.ui +++ b/src/portcontrol.ui @@ -317,6 +317,24 @@ + + cbPortList + pbReloadPorts + pbOpenPort + cbBaudRate + rbNoParity + rbOddParity + rbEvenParity + rb8Bits + rb7Bits + rb6Bits + rb5Bits + rb1StopBit + rb2StopBit + rbNoFlowControl + rbHardwareControl + rbSoftwareControl + diff --git a/src/recordpanel.cpp b/src/recordpanel.cpp new file mode 100644 --- /dev/null +++ b/src/recordpanel.cpp @@ -0,0 +1,294 @@ +/* + 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 "recordpanel.h" +#include "ui_recordpanel.h" +#include "setting_defines.h" + +RecordPanel::RecordPanel(DataRecorder* recorder, ChannelManager* channelMan, QWidget *parent) : + QWidget(parent), + ui(new Ui::RecordPanel), + recordToolBar(tr("Record Toolbar")), + recordAction(QIcon::fromTheme("media-record"), tr("Record"), this) +{ + overwriteSelected = false; + _recorder = recorder; + _channelMan = channelMan; + + ui->setupUi(this); + + recordToolBar.setObjectName("tbRecord"); + + recordAction.setCheckable(true); + recordToolBar.addAction(&recordAction); + ui->pbRecord->setDefaultAction(&recordAction); + + connect(ui->pbBrowse, &QPushButton::clicked, + this, &RecordPanel::selectFile); + connect(&recordAction, &QAction::triggered, + this, &RecordPanel::onRecord); + + connect(ui->cbRecordPaused, SIGNAL(toggled(bool)), + this, SIGNAL(recordPausedChanged(bool))); + + connect(ui->cbDisableBuffering, &QCheckBox::toggled, + [this](bool enabled) + { + _recorder->disableBuffering = enabled; + }); + + connect(ui->cbWindowsLE, &QCheckBox::toggled, + [this](bool enabled) + { + _recorder->windowsLE = enabled; + }); + + connect(&recordAction, &QAction::toggled, ui->cbWindowsLE, &QWidget::setDisabled); +} + +RecordPanel::~RecordPanel() +{ + delete ui; +} + +QToolBar* RecordPanel::toolbar() +{ + return &recordToolBar; +} + +bool RecordPanel::isRecording() +{ + return recordAction.isChecked(); +} + +bool RecordPanel::recordPaused() +{ + return ui->cbRecordPaused->isChecked(); +} + +bool RecordPanel::selectFile() +{ + QString fileName = QFileDialog::getSaveFileName( + parentWidget(), tr("Select recording file")); + + if (fileName.isEmpty()) + { + return false; + } + else + { + selectedFile = fileName; + ui->lbFileName->setText(selectedFile); + overwriteSelected = QFile::exists(fileName); + return true; + } +} + +void RecordPanel::onRecord(bool start) +{ + if (!start) + { + stopRecording(); + return; + } + + bool canceled = false; + if (ui->leSeparator->text().isEmpty()) + { + QMessageBox::critical(this, "Error", + "Column separator cannot be empty! Please select a separator."); + ui->leSeparator->setFocus(Qt::OtherFocusReason); + canceled = true; + } + + // check file name + if (!canceled && selectedFile.isEmpty() && !selectFile()) + { + canceled = true; + } + + if (!canceled && !overwriteSelected && QFile::exists(selectedFile)) + { + if (ui->cbAutoIncrement->isChecked()) + { + // TODO: should we increment even if user selected to replace? + canceled = !incrementFileName(); + } + else + { + canceled = !confirmOverwrite(selectedFile); + } + } + + if (canceled) + { + recordAction.setChecked(false); + } + else + { + overwriteSelected = false; + startRecording(); + } +} + +bool RecordPanel::incrementFileName(void) +{ + QFileInfo fileInfo(selectedFile); + + QString base = fileInfo.completeBaseName(); + QRegularExpression regex("(.*?)(\\d+)(?!.*\\d)(.*)"); + auto match = regex.match(base); + + if (match.hasMatch()) + { + bool ok; + int fileNum = match.captured(2).toInt(&ok); + base = match.captured(1) + QString::number(fileNum + 1) + match.captured(3); + } + else + { + base += "_1"; + } + + QString suffix = fileInfo.suffix();; + if (!suffix.isEmpty()) + { + suffix = "." + suffix; + } + + QString autoFileName = fileInfo.path() + "/" + base + suffix; + + // check if auto generated file name exists, ask user another name + if (QFile::exists(autoFileName)) + { + if (!confirmOverwrite(autoFileName)) + { + return false; + } + } + else + { + selectedFile = autoFileName; + } + + ui->lbFileName->setText(selectedFile); + return true; +} + +bool RecordPanel::confirmOverwrite(QString fileName) +{ + // prepare message box + QMessageBox mb(parentWidget()); + mb.setWindowTitle(tr("File Already Exists")); + mb.setIcon(QMessageBox::Warning); + mb.setText(tr("File (%1) already exists. How to continue?").arg(fileName)); + + auto bCancel = mb.addButton(QMessageBox::Cancel); + auto bOverwrite = mb.addButton(tr("Overwrite"), QMessageBox::DestructiveRole); + mb.addButton(tr("Select Another File"), QMessageBox::YesRole); + + mb.setEscapeButton(bCancel); + + // show message box + mb.exec(); + + if (mb.clickedButton() == bCancel) + { + return false; + } + else if (mb.clickedButton() == bOverwrite) + { + selectedFile = fileName; + return true; + } + else // select button + { + return selectFile(); + } +} + +void RecordPanel::startRecording(void) +{ + QStringList channelNames; + if (ui->cbHeader->isChecked()) + { + channelNames = _channelMan->infoModel()->channelNames(); + } + _recorder->startRecording(selectedFile, getSeparator(), channelNames); + emit recordStarted(); +} + +void RecordPanel::stopRecording(void) +{ + emit recordStopped(); + _recorder->stopRecording(); +} + +void RecordPanel::onPortClose() +{ + if (recordAction.isChecked() && ui->cbStopOnClose->isChecked()) + { + stopRecording(); + recordAction.setChecked(false); + } +} + +QString RecordPanel::getSeparator() const +{ + QString sep = ui->leSeparator->text(); + sep.replace("\\t", "\t"); + return sep; +} + +void RecordPanel::saveSettings(QSettings* settings) +{ + settings->beginGroup(SettingGroup_Record); + settings->setValue(SG_Record_AutoIncrement, ui->cbAutoIncrement->isChecked()); + settings->setValue(SG_Record_RecordPaused, ui->cbRecordPaused->isChecked()); + settings->setValue(SG_Record_StopOnClose, ui->cbStopOnClose->isChecked()); + settings->setValue(SG_Record_Header, ui->cbHeader->isChecked()); + settings->setValue(SG_Record_DisableBuffering, ui->cbDisableBuffering->isChecked()); + settings->setValue(SG_Record_Separator, ui->leSeparator->text()); + settings->endGroup(); +} + +void RecordPanel::loadSettings(QSettings* settings) +{ + settings->beginGroup(SettingGroup_Record); + ui->cbAutoIncrement->setChecked( + settings->value(SG_Record_AutoIncrement, ui->cbAutoIncrement->isChecked()).toBool()); + ui->cbRecordPaused->setChecked( + settings->value(SG_Record_RecordPaused, ui->cbRecordPaused->isChecked()).toBool()); + ui->cbStopOnClose->setChecked( + settings->value(SG_Record_StopOnClose, ui->cbStopOnClose->isChecked()).toBool()); + ui->cbHeader->setChecked( + settings->value(SG_Record_Header, ui->cbHeader->isChecked()).toBool()); + ui->cbDisableBuffering->setChecked( + settings->value(SG_Record_DisableBuffering, ui->cbDisableBuffering->isChecked()).toBool()); + ui->leSeparator->setText(settings->value(SG_Record_Separator, ui->leSeparator->text()).toString()); + settings->endGroup(); +} diff --git a/src/recordpanel.h b/src/recordpanel.h new file mode 100644 --- /dev/null +++ b/src/recordpanel.h @@ -0,0 +1,117 @@ +/* + 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 RECORDPANEL_H +#define RECORDPANEL_H + +#include +#include +#include +#include + +#include "datarecorder.h" +#include "channelmanager.h" + +namespace Ui { +class RecordPanel; +} + +class RecordPanel : public QWidget +{ + Q_OBJECT + +public: + explicit RecordPanel(DataRecorder* recorder, ChannelManager* channelMan, + QWidget* parent = 0); + ~RecordPanel(); + + QToolBar* toolbar(); + + bool isRecording(); + bool recordPaused(); + + /// Stores settings into a `QSettings` + void saveSettings(QSettings* settings); + /// Loads settings from a `QSettings`. + void loadSettings(QSettings* settings); + +signals: + void recordStarted(); + void recordStopped(); + void recordPausedChanged(bool enabled); + +public slots: + /// Must be called when port is closed + void onPortClose(); + +private: + Ui::RecordPanel *ui; + QToolBar recordToolBar; + QAction recordAction; + QString selectedFile; + bool overwriteSelected; + DataRecorder* _recorder; + ChannelManager* _channelMan; + + /** + * @brief Increments the file name. + * + * If file name doesn't have a number at the end of it, a number is appended + * with underscore starting from 1. + * + * @return false if user cancels + */ + bool incrementFileName(void); + + /** + * @brief Used to ask user confirmation if auto generated file + * name exists. + * + * If user confirms overwrite, `selectedFile` is set to + * `fileName`. User is also given option to select file and is + * shown a file select dialog in this case. + * + * @param fileName auto generated file name. + * @return false if user cancels + */ + bool confirmOverwrite(QString fileName); + + void startRecording(void); + void stopRecording(void); + + /// Returns separator text from ui. "\t" is converted to TAB + /// character. + QString getSeparator() const; + +private slots: + /** + * @brief Opens up the file select dialog + * + * If you cancel the selection operation, currently selected file is not + * changed. + * + * @return true if file selected, false if user cancels + */ + bool selectFile(); + + void onRecord(bool start); + +}; + +#endif // RECORDPANEL_H diff --git a/src/recordpanel.ui b/src/recordpanel.ui new file mode 100644 --- /dev/null +++ b/src/recordpanel.ui @@ -0,0 +1,227 @@ + + + RecordPanel + + + + 0 + 0 + 627 + 261 + + + + Form + + + + + + + + + + Select record file + + + Browse + + + + + + + + 0 + 0 + + + + Select file... + + + + + + + + + + + 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 + + + + + + + + + + + + Column Separator: + + + + + + + + 30 + 16777215 + + + + For TAB character enter \t + + + , + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 1 + + + + + + + + + + + + + 85 + 50 + + + + Start/Stop Recording + + + Record + + + + + + + Qt::Vertical + + + + 20 + 1 + + + + + + + + + + + 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 @@ -1,5 +1,5 @@ /* - Copyright © 2016 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -29,6 +29,7 @@ const char SettingGroup_CustomFrame[] = const char SettingGroup_Channels[] = "Channels"; const char SettingGroup_Plot[] = "Plot"; const char SettingGroup_Commands[] = "Commands"; +const char SettingGroup_Record[] = "Record"; // mainwindow setting keys const char SG_MainWindow_Size[] = "size"; @@ -70,9 +71,14 @@ const char SG_CustomFrame_DebugMode[] = // channel manager keys const char SG_Channels_Channel[] = "channel"; const char SG_Channels_Name[] = "name"; +const char SG_Channels_Color[] = "color"; +const char SG_Channels_Visible[] = "visible"; // 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"; @@ -88,4 +94,12 @@ const char SG_Commands_Name[] = "name"; const char SG_Commands_Type[] = "type"; const char SG_Commands_Data[] = "data"; +// record panel settings keys +const char SG_Record_AutoIncrement[] = "autoIncrement"; +const char SG_Record_RecordPaused[] = "recordPaused"; +const char SG_Record_StopOnClose[] = "stopOnClose"; +const char SG_Record_Header[] = "header"; +const char SG_Record_Separator[] = "separator"; +const char SG_Record_DisableBuffering[] = "disableBuffering"; + #endif // SETTING_DEFINES_H diff --git a/src/snapshot.cpp b/src/snapshot.cpp --- a/src/snapshot.cpp +++ b/src/snapshot.cpp @@ -24,8 +24,9 @@ #include "snapshot.h" #include "snapshotview.h" -Snapshot::Snapshot(QMainWindow* parent, QString name) : +Snapshot::Snapshot(QMainWindow* parent, QString name, ChannelInfoModel infoModel) : QObject(parent), + cInfoModel(infoModel), _showAction(this), _deleteAction("&Delete", this) { @@ -106,14 +107,14 @@ void Snapshot::setName(QString name) emit nameChanged(this); } -void Snapshot::setChannelNames(QStringList names) +ChannelInfoModel* Snapshot::infoModel() { - _channelNames = names; + return &cInfoModel; } QString Snapshot::channelName(unsigned channel) { - return _channelNames[channel]; + return cInfoModel.name(channel); } void Snapshot::save(QString fileName) diff --git a/src/snapshot.h b/src/snapshot.h --- a/src/snapshot.h +++ b/src/snapshot.h @@ -27,6 +27,8 @@ #include #include +#include "channelinfomodel.h" + class SnapshotView; class Snapshot : public QObject @@ -34,7 +36,7 @@ class Snapshot : public QObject Q_OBJECT public: - Snapshot(QMainWindow* parent, QString name); + Snapshot(QMainWindow* parent, QString name, ChannelInfoModel infoModel); ~Snapshot(); QVector> data; @@ -43,8 +45,8 @@ public: QString name(); QString displayName(); ///< `name()` plus '*' if snapshot is not saved + ChannelInfoModel* infoModel(); void setName(QString name); - void setChannelNames(QStringList names); // must be called when setting data! QString channelName(unsigned channel); void save(QString fileName); ///< save snapshot data as CSV @@ -56,7 +58,7 @@ signals: private: QString _name; - QStringList _channelNames; + ChannelInfoModel cInfoModel; QAction _showAction; QAction _deleteAction; QMainWindow* mainWindow; diff --git a/src/snapshotmanager.cpp b/src/snapshotmanager.cpp --- a/src/snapshotmanager.cpp +++ b/src/snapshotmanager.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2015 Hasan Yavuz Özderya + Copyright © 2017 Hasan Yavuz Özderya This file is part of serialplot. @@ -24,6 +24,7 @@ #include #include #include +#include #include #include "snapshotmanager.h" @@ -40,6 +41,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)), @@ -63,7 +65,7 @@ SnapshotManager::~SnapshotManager() Snapshot* SnapshotManager::makeSnapshot() { QString name = QTime::currentTime().toString("'Snapshot ['HH:mm:ss']'"); - auto snapshot = new Snapshot(_mainWindow, name); + auto snapshot = new Snapshot(_mainWindow, name, *(_channelMan->infoModel())); unsigned numOfChannels = _channelMan->numOfChannels(); unsigned numOfSamples = _channelMan->numOfSamples(); @@ -76,7 +78,6 @@ Snapshot* SnapshotManager::makeSnapshot( snapshot->data[ci][i] = QPointF(i, _channelMan->channelBuffer(ci)->sample(i)); } } - snapshot->setChannelNames(_channelMan->channelNames()->stringList()); return snapshot; } @@ -189,9 +190,11 @@ void SnapshotManager::loadSnapshotFromFi lineNum++; } - auto snapshot = new Snapshot(_mainWindow, QFileInfo(fileName).baseName()); + ChannelInfoModel channelInfo(channelNames); + + auto snapshot = new Snapshot( + _mainWindow, QFileInfo(fileName).baseName(), ChannelInfoModel(channelNames)); snapshot->data = data; - snapshot->setChannelNames(channelNames); addSnapshot(snapshot, false); } diff --git a/src/snapshotview.cpp b/src/snapshotview.cpp --- a/src/snapshotview.cpp +++ b/src/snapshotview.cpp @@ -29,7 +29,7 @@ SnapshotView::SnapshotView(QWidget *pare ui->setupUi(this); - plotMan = new PlotManager(ui->plotArea); + plotMan = new PlotManager(ui->plotArea, snapshot->infoModel()); ui->menuSnapshot->insertAction(ui->actionClose, snapshot->deleteAction()); this->setWindowTitle(snapshot->displayName()); @@ -46,7 +46,7 @@ SnapshotView::SnapshotView(QWidget *pare connect(ui->actionRename, &QAction::triggered, this, &SnapshotView::showRenameDialog); - connect(ui->actionExport, &QAction::triggered, + connect(ui->actionSave, &QAction::triggered, this, &SnapshotView::save); // add 'View' menu items diff --git a/src/snapshotview.ui b/src/snapshotview.ui --- a/src/snapshotview.ui +++ b/src/snapshotview.ui @@ -26,7 +26,7 @@ 0 0 544 - 20 + 25 @@ -34,7 +34,7 @@ &Snapshot - + @@ -45,9 +45,9 @@ - + - &Export CSV + &Save as CSV Save snapshot as CSV file diff --git a/src/version.h b/src/version.h new file mode 100644 --- /dev/null +++ b/src/version.h @@ -0,0 +1,31 @@ +/* + 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 +#define VERSION_STRING "0.9.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/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