diff --git a/lumberjack.pro b/lumberjack.pro index 6cc3bbe..8027def 100644 --- a/lumberjack.pro +++ b/lumberjack.pro @@ -32,6 +32,7 @@ INCLUDEPATH += src \ plugins/cobsr SOURCES += \ + src/data_source_manager.cpp \ src/fft_sampler.cpp \ src/fft_widget.cpp \ src/helpers.cpp \ @@ -44,6 +45,7 @@ SOURCES += \ src/plot_widget.cpp \ src/main.cpp \ src/mainwindow.cpp \ + src/plugins/plugin_exporter.cpp \ src/plugins/plugin_importer.cpp \ src/plugins/plugin_registry.cpp \ src/widgets/about_dialog.cpp \ @@ -59,6 +61,7 @@ SOURCES += \ src/widgets/timeline_widget.cpp HEADERS += \ + src/data_source_manager.hpp \ src/mainwindow.h \ src/fft_sampler.hpp \ src/fft_widget.hpp \ diff --git a/lumberjack_resource.rc b/lumberjack_resource.rc index e2b39f6..0d567e6 100644 --- a/lumberjack_resource.rc +++ b/lumberjack_resource.rc @@ -1,6 +1,6 @@ #include -IDI_ICON1 ICON DISCARDABLE "C:\\lumberjack\\logo\\lumberjack.ico" +IDI_ICON1 ICON ".\\logo\\lumberjack.ico" VS_VERSION_INFO VERSIONINFO FILEVERSION 0,0,0,0 @@ -11,9 +11,9 @@ VS_VERSION_INFO VERSIONINFO #else FILEFLAGS 0x0L #endif - FILEOS VOS__WINDOWS32 + FILEOS VOS_NT_WINDOWS32 FILETYPE VFT_DLL - FILESUBTYPE 0x0L + FILESUBTYPE VFT2_UNKNOWN BEGIN BLOCK "StringFileInfo" BEGIN @@ -26,6 +26,9 @@ VS_VERSION_INFO VERSIONINFO VALUE "OriginalFilename", "lumberjack.exe\0" VALUE "ProductName", "lumberjack\0" VALUE "ProductVersion", "0.0.0.0\0" + VALUE "InternalName", "\0" + VALUE "Comments", "\0" + VALUE "LegalTrademarks", "\0" END END BLOCK "VarFileInfo" diff --git a/plugins/csv_exporter/csv_exporter.pro b/plugins/csv_exporter/csv_exporter.pro index 489e2c2..74dd6c5 100644 --- a/plugins/csv_exporter/csv_exporter.pro +++ b/plugins/csv_exporter/csv_exporter.pro @@ -1,4 +1,4 @@ -QT += gui +QT += gui widgets TEMPLATE = lib DEFINES += CSV_EXPORTER_LIBRARY @@ -15,11 +15,14 @@ INCLUDEPATH += \ HEADERS += \ csv_exporter_global.h \ lumberjack_csv_exporter.hpp \ + ../../src/data_series.hpp \ ../../src/plugins/plugin_base.hpp \ ../../src/plugins/plugin_exporter.hpp \ SOURCES += \ - lumberjack_csv_exporter.cpp + lumberjack_csv_exporter.cpp \ + ../../src/data_series.cpp \ + ../../src/plugins/plugin_exporter.cpp # Default rules for deployment. unix { diff --git a/plugins/csv_exporter/lumberjack_csv_exporter.cpp b/plugins/csv_exporter/lumberjack_csv_exporter.cpp index 17665e7..597690c 100644 --- a/plugins/csv_exporter/lumberjack_csv_exporter.cpp +++ b/plugins/csv_exporter/lumberjack_csv_exporter.cpp @@ -1,5 +1,6 @@ -#include "lumberjack_csv_exporter.hpp" +#include +#include "lumberjack_csv_exporter.hpp" LumberjackCSVExporter::LumberjackCSVExporter() @@ -18,3 +19,197 @@ QStringList LumberjackCSVExporter::supportedFileTypes() const return fileTypes; } + +bool LumberjackCSVExporter::beforeExport(void) +{ + // TODO: Set export options + return true; +} + + +/* + * Export the provided series to a CSV file + */ +bool LumberjackCSVExporter::exportData(QList &series, QStringList &errors) +{ + if (m_filename.isEmpty()) + { + errors.append(tr("Filename is empty")); + return false; + } + + QFile outputFile(m_filename); + + if (!outputFile.open(QIODevice::WriteOnly) || !outputFile.isOpen() || !outputFile.isWritable()) + { + errors.append(tr("Could not open file for writing")); + return false; + } + + // Copy across data series + m_data.clear(); + m_indices.clear(); + + for (auto s : series) + { + if (!s.isNull()) + { + m_data.append(s); + m_indices.append(0); + } + } + + QStringList row; + + // Write header row + row = headerRow(); + outputFile.write(rowToString(row)); + + if (m_unitsRow) + { + row = unitsRow(); + outputFile.write(rowToString(row)); + } + + bool valid = true; + + while (valid) + { + row = nextDataRow(valid); + + if (valid) + { + outputFile.write(rowToString(row)); + } + } + + outputFile.close(); + + return true; +} + + +QByteArray LumberjackCSVExporter::rowToString(QStringList &row) const +{ + QString data = row.join(m_delimiter).trimmed() + "\n"; + + return data.toLatin1(); +} + + +/** + * @brief LumberjackCSVExporter::headerRow - Generate a header row for the exported dataset + * @return + */ +QStringList LumberjackCSVExporter::headerRow(void) const +{ + QStringList header; + + header << tr("Timestamp"); + + for (auto series : m_data) + { + if (series.isNull()) continue; + + header.append(series->getLabel()); + } + + return header; +} + + +/** + * @brief LumberjackCSVExporter::unitsRow - Generate a units row for the exported dataset + * @return + */ +QStringList LumberjackCSVExporter::unitsRow(void) const +{ + QStringList units; + + units << QString(""); + + for (auto series : m_data) + { + if (series.isNull()) continue; + + units.append(series->getUnits()); + } + + return units; +} + + +/** + * @brief LumberjackCSVExporter::nextDataRow - Generate the next row of data + * @param valid + * @return + */ +QStringList LumberjackCSVExporter::nextDataRow(bool &valid) +{ + // TODO: Better configuration of timestamp resolution... + const double DT = 1e-6; + + QStringList values; + + double nextTimestamp = LONG_MAX; + + int N = qMin(m_data.count(), m_indices.count()); + + bool dataAvailable = false; + + // What is the *smallest* timestamp? + for (int ii = 0; ii < N; ii++) + { + auto series = m_data.at(ii); + uint64_t idx = m_indices.at(ii); + + if (idx < series->size()) + { + dataAvailable = true; + + auto point = series->getDataPoint(idx); + + if (point.timestamp < nextTimestamp) + { + nextTimestamp = point.timestamp; + } + } + } + + // No more data available + if (!dataAvailable) + { + valid = false; + return values; + } + + // Timestamp + values.append(QString::number(nextTimestamp)); + + // Now, iterate through each series, and see if it has a corresponding value at this timestamp + for (int ii = 0; ii < N; ii++) + { + auto series = m_data.at(ii); + uint64_t idx = m_indices.at(ii); + + QString value; + + if (idx < series->size()) + { + auto point = series->getDataPoint(idx); + + // Timestamp is within allowable range + if (point.timestamp <= (nextTimestamp + DT)) + { + value = QString::number(point.value); + m_indices[ii]++; + } + } + + // TODO: "Empty" value compensation? + values.append(value); + } + + valid = true; + return values; +} diff --git a/plugins/csv_exporter/lumberjack_csv_exporter.hpp b/plugins/csv_exporter/lumberjack_csv_exporter.hpp index c676bc9..8e93b38 100644 --- a/plugins/csv_exporter/lumberjack_csv_exporter.hpp +++ b/plugins/csv_exporter/lumberjack_csv_exporter.hpp @@ -18,14 +18,33 @@ class CSV_EXPORTER_EXPORT LumberjackCSVExporter : public ExportPlugin virtual QString pluginDescription(void) const override { return m_description; } virtual QString pluginVersion(void) const override { return m_version; } - // Importer plugin functionality + // Exporter plugin functionality virtual QStringList supportedFileTypes(void) const override; + virtual bool beforeExport(void) override; + virtual bool exportData(QList &series, QStringList &errors) override; + protected: const QString m_name = "CSV Exporter"; const QString m_description = "Export data to CSV file"; const QString m_version = "0.1.0"; + QList m_data; + QList m_indices; + + // Data export options + QString m_delimiter = ","; + + bool m_zeroTimestamp = false; + bool m_unitsRow = false; + + // Internal helper functions + QByteArray rowToString(QStringList &row) const; + QStringList headerRow(void) const; + QStringList unitsRow(void) const; + + QStringList nextDataRow(bool &valid); + }; #endif // LUMBERJACK_CSV_IMPORTER_HPP diff --git a/plugins/csv_importer/csv_importer.pro b/plugins/csv_importer/csv_importer.pro index e279971..5519d8c 100644 --- a/plugins/csv_importer/csv_importer.pro +++ b/plugins/csv_importer/csv_importer.pro @@ -13,11 +13,11 @@ INCLUDEPATH += \ ../../src/plugins HEADERS += \ - ../../src/data_series.hpp \ - csv_import_options.hpp \ csv_importer_global.h \ - import_options_dialog.hpp \ lumberjack_csv_importer.hpp \ + import_options_dialog.hpp \ + csv_import_options.hpp \ + ../../src/data_series.hpp \ ../../src/plugins/plugin_base.hpp \ ../../src/plugins/plugin_importer.hpp \ diff --git a/plugins/csv_importer/lumberjack_csv_importer.cpp b/plugins/csv_importer/lumberjack_csv_importer.cpp index fbe3da2..b4dc9ef 100644 --- a/plugins/csv_importer/lumberjack_csv_importer.cpp +++ b/plugins/csv_importer/lumberjack_csv_importer.cpp @@ -32,7 +32,7 @@ QStringList LumberjackCSVImporter::supportedFileTypes() const * @brief LumberjackCSVImporter::beforeLoadData - Open configuration dialog * @return */ -bool LumberjackCSVImporter::beforeLoadData(void) +bool LumberjackCSVImporter::beforeImport(void) { CSVImportOptionsDialog dlg(m_filename); @@ -53,7 +53,7 @@ bool LumberjackCSVImporter::beforeLoadData(void) * @param errors - * @return */ -bool LumberjackCSVImporter::loadDataFile(QStringList &errors) +bool LumberjackCSVImporter::importData(QStringList &errors) { // Reset importer to initial conditions m_headers.clear(); diff --git a/plugins/csv_importer/lumberjack_csv_importer.hpp b/plugins/csv_importer/lumberjack_csv_importer.hpp index f2ec604..41b43b0 100644 --- a/plugins/csv_importer/lumberjack_csv_importer.hpp +++ b/plugins/csv_importer/lumberjack_csv_importer.hpp @@ -23,8 +23,8 @@ class CSV_IMPORTER_EXPORT LumberjackCSVImporter : public ImportPlugin // Importer plugin functionality virtual QStringList supportedFileTypes(void) const override; - virtual bool beforeLoadData(void) override; - virtual bool loadDataFile(QStringList &errors) override; + virtual bool beforeImport(void) override; + virtual bool importData(QStringList &errors) override; virtual QList> getDataSeries(void) const override; diff --git a/src/data_series.hpp b/src/data_series.hpp index b889ff7..a99a4b5 100644 --- a/src/data_series.hpp +++ b/src/data_series.hpp @@ -230,5 +230,7 @@ public slots: }; +typedef QSharedPointer DataSeriesPointer; + #endif // DATA_SERIES_H diff --git a/src/data_source.cpp b/src/data_source.cpp index d3d80c1..fd699c1 100644 --- a/src/data_source.cpp +++ b/src/data_source.cpp @@ -6,7 +6,8 @@ #include "data_source.hpp" -DataSource::DataSource(QString label, QString description) : +DataSource::DataSource(QString source, QString label, QString description) : + m_source(source), m_label(label), m_description(description) { @@ -19,6 +20,16 @@ DataSource::~DataSource() } +/* + * Return a unique identifier string for this data source + */ +QString DataSource::getIdentifier(void) const +{ + // TODO: Maybe a "better" approach to this in the future? + return m_source + ":" + m_label + ":" + m_description; +} + + /** * @brief DataSource::getColorWheel creates a list of "distinct" colors * @return a list of colors @@ -131,7 +142,7 @@ QStringList DataSource::getSeriesLabels(QString filter_string) const * * Returns true if the series was added, else false */ -bool DataSource::addSeries(QSharedPointer series, bool auto_color) +bool DataSource::addSeries(DataSeriesPointer series, bool auto_color) { if (series.isNull()) { @@ -163,7 +174,7 @@ bool DataSource::addSeries(QSharedPointer series, bool auto_color) bool DataSource::addSeries(DataSeries *series, bool auto_color) { - return addSeries(QSharedPointer(series), auto_color); + return addSeries(DataSeriesPointer(series), auto_color); } @@ -186,7 +197,7 @@ bool DataSource::addSeries(QString label, bool auto_color) * * If the index is out of bounds, return a null DataSeries pointer */ -QSharedPointer DataSource::getSeriesByIndex(unsigned int index) +DataSeriesPointer DataSource::getSeriesByIndex(unsigned int index) { QList keys = data_series.keys(); @@ -196,11 +207,11 @@ QSharedPointer DataSource::getSeriesByIndex(unsigned int index) } // No match found - return QSharedPointer(nullptr); + return DataSeriesPointer(nullptr); } -bool DataSource::removeSeries(QSharedPointer series, bool update) +bool DataSource::removeSeries(DataSeriesPointer series, bool update) { for (QString label : data_series.keys()) { @@ -227,7 +238,7 @@ bool DataSource::removeSeries(QSharedPointer series, bool update) * * If the label is not found, return a null DataSeries pointer */ -QSharedPointer DataSource::getSeriesByLabel(QString label) +DataSeriesPointer DataSource::getSeriesByLabel(QString label) { if (data_series.contains(label)) { @@ -235,7 +246,7 @@ QSharedPointer DataSource::getSeriesByLabel(QString label) } // No match found - return a null series - return QSharedPointer(nullptr); + return DataSeriesPointer(nullptr); } @@ -289,192 +300,3 @@ QStringList DataSource::getGroupLabels() const } -DataSourceManager *DataSourceManager::instance = 0; - - -DataSourceManager::DataSourceManager() -{ -} - - -DataSourceManager::~DataSourceManager() -{ - removeAllSources(false); -} - - -QSharedPointer DataSourceManager::findSeries(QString source_label, QString series_label) -{ - auto source = getSourceByLabel(source_label); - - if (source.isNull()) return QSharedPointer(nullptr); - - return source->getSeriesByLabel(series_label); -} - - -QStringList DataSourceManager::getSourceLabels() const -{ - QStringList labels; - - for (auto src : sources) - { - if (src.isNull()) continue; - - labels.push_back(src->getLabel()); - } - - return labels; -} - - -QSharedPointer DataSourceManager::getSourceByIndex(unsigned int idx) -{ - if (idx < sources.size()) return sources.at(idx); - - return QSharedPointer(nullptr); -} - - -QSharedPointer DataSourceManager::getSourceByLabel(QString label) -{ - for (auto src : sources) - { - if (src.isNull()) continue; - - if (src->getLabel() == label) - { - return src; - } - } - - return QSharedPointer(nullptr); -} - - -bool DataSourceManager::addSource(QSharedPointer source) -{ - if (source.isNull()) return false; - - for (auto src : sources) - { - if (src == source) - { - qInfo() << "Ignoring duplicate source:" << source->getLabel(); - - return false; - } - } - - sources.push_back(source); - - connect(source.data(), &DataSource::dataChanged, this, &DataSourceManager::onDataChanged); - - emit sourcesChanged(); - - return true; -} - - -/** - * @brief DataSourceManager::addSource adds a DataSource with the given title - * @param label is the label of the DataSource to add - * @return true if the source was added, false if a DataSource with the given title already existed - */ -bool DataSourceManager::addSource(QString label, QString description) -{ - if (getSourceByLabel(label).isNull()) - { - return addSource(new DataSource(label, description)); - } - - // Source with provided label already exists! - return false; -} - - -/** - * @brief DataSourceManager::removeSource removes the DataSource from this DataSourceManager - * @param source is a shared pointer to the DataSource - * @return true if the source was removed, else false - */ -bool DataSourceManager::removeSource(QSharedPointer source) -{ - for (auto idx = 0; idx < sources.size(); idx++) - { - auto src = sources.at(idx); - - if (src == source) - { - sources.removeAt(idx); - emit sourcesChanged(); - return true; - } - } - - return false; -} - - -/** - * @brief DataSourceManager::removeSourceByIndex removes the DataSource at the specified index - * @param idx - Index of the DataSource to remove - * @param update - If true, emit a "sourcesChanged" event - * @return true if the source was removed, else false (in the case of an invalid index) - */ -bool DataSourceManager::removeSourceByIndex(unsigned int idx, bool update) -{ - if (idx < sources.size()) - { - sources.removeAt(idx); - - if (update) - { - emit sourcesChanged(); - } - - return true; - } - - return false; -} - - -/** - * @brief DataSourceManager::removeSourceByLabel - Remove the first DataSource which matches the specified label - * @param label - QString label - * @param update - If true, emit a "sourcesChanged" event - * @return true if a source matching the provided label was found (and subsequentyly deleted) - */ -bool DataSourceManager::removeSourceByLabel(QString label, bool update) -{ - for (int idx = 0; idx < sources.size(); idx++) - { - auto src = sources.at(idx); - - if (!src.isNull() && src->getLabel() == label) - { - return removeSourceByIndex(idx, update); - } - } - - return false; -} - - -/** - * @brief DataSourceManager::removeAllSources removes all DataSource objects from this DataSourceManager - * @param update - if true, emit a "sourcesChanged" event - */ -void DataSourceManager::removeAllSources(bool update) -{ - while (sources.count() > 0) - { - removeSourceByIndex(0, update); - } - - if (update) - { - emit sourcesChanged(); - } -} diff --git a/src/data_source.hpp b/src/data_source.hpp index fdd07e8..3296c2b 100644 --- a/src/data_source.hpp +++ b/src/data_source.hpp @@ -26,9 +26,13 @@ class DataSource : public QObject public: - DataSource(QString label, QString description = QString()); + DataSource(QString source, QString label, QString description = QString()); virtual ~DataSource(); + QString getIdentifier(void) const; + + QString getSource(void) const { return m_source; } + QString getLabel(void) const { return m_label; } // Descriptive text for this data source (override for custom sources) @@ -40,14 +44,14 @@ class DataSource : public QObject QStringList getGroupLabels(void) const; - bool addSeries(QSharedPointer series, bool auto_color = true); + bool addSeries(DataSeriesPointer series, bool auto_color = true); bool addSeries(DataSeries* series, bool auto_color=true); bool addSeries(QString label, bool auto_color=true); - QSharedPointer getSeriesByIndex(unsigned int index); - QSharedPointer getSeriesByLabel(QString label); + DataSeriesPointer getSeriesByIndex(unsigned int index); + DataSeriesPointer getSeriesByLabel(QString label); - bool removeSeries(QSharedPointer series, bool update = true); + bool removeSeries(DataSeriesPointer series, bool update = true); bool removeSeriesByLabel(QString label, bool update = true); void removeAllSeries(bool update = true); @@ -57,6 +61,9 @@ class DataSource : public QObject protected: + //! Data source (e.g plugin name) + QString m_source; + //! Text label associated with this DataSource (e.g. filename) QString m_label; @@ -72,77 +79,11 @@ class DataSource : public QObject int color_wheel_cursor = 0; // Keep a map of label:series for efficient lookup - QMap> data_series; + QMap data_series; }; -/* - * Data source manager class: - * - Manages all data sources - * - Employs singleton design pattern so can be accessed globally - */ -class DataSourceManager : public QObject -{ - Q_OBJECT - - static DataSourceManager *instance; - -public: - DataSourceManager(); - ~DataSourceManager(); - - static DataSourceManager *getInstance() - { - if (!instance) - { - instance = new DataSourceManager; - } - - return instance; - } - - static void cleanup() - { - if (instance) - { - delete instance; - instance = nullptr; - } - } - -public slots: - - QSharedPointer findSeries(QString source_label, QString series_label); - - int getSourceCount(void) const { return sources.size(); } - QStringList getSourceLabels(void) const; - - QSharedPointer getSourceByIndex(unsigned int idx); - QSharedPointer getSourceByLabel(QString label); - - bool addSource(QSharedPointer source); - bool addSource(DataSource* source) { return addSource(QSharedPointer(source)); } - bool addSource(QString label, QString description = QString()); - - bool removeSource(QSharedPointer source); - bool removeSource(DataSource* source) { return removeSource(QSharedPointer(source)); } - - bool removeSourceByIndex(unsigned int idx, bool update = true); - bool removeSourceByLabel(QString label, bool update = true); - - void removeAllSources(bool update = true); - - void update(void) { emit sourcesChanged(); } - -signals: - void sourcesChanged(); - -protected slots: - void onDataChanged() { emit sourcesChanged(); } - -protected: - QVector> sources; -}; +typedef QSharedPointer DataSourcePointer; #endif // DATA_SOURCE_H diff --git a/src/data_source_manager.cpp b/src/data_source_manager.cpp new file mode 100644 index 0000000..bb9e456 --- /dev/null +++ b/src/data_source_manager.cpp @@ -0,0 +1,386 @@ +#include +#include + + +#include "data_source_manager.hpp" + +#include "plugin_registry.hpp" +#include "lumberjack_settings.hpp" + + +DataSourceManager *DataSourceManager::instance = 0; + + +DataSourceManager::DataSourceManager() +{ +} + + +DataSourceManager::~DataSourceManager() +{ + removeAllSources(false); +} + + +DataSeriesPointer DataSourceManager::findSeries(QString source_label, QString series_label) +{ + auto source = getSourceByLabel(source_label); + + if (source.isNull()) return DataSeriesPointer(nullptr); + + return source->getSeriesByLabel(series_label); +} + + +QStringList DataSourceManager::getSourceLabels() const +{ + QStringList labels; + + for (auto src : sources) + { + if (src.isNull()) continue; + + labels.push_back(src->getLabel()); + } + + return labels; +} + + +DataSourcePointer DataSourceManager::getSourceByIndex(unsigned int idx) +{ + if (idx < sources.size()) return sources.at(idx); + + return DataSourcePointer(nullptr); +} + + +DataSourcePointer DataSourceManager::getSourceByLabel(QString label) +{ + for (auto src : sources) + { + if (src.isNull()) continue; + + if (src->getLabel() == label) + { + return src; + } + } + + return DataSourcePointer(nullptr); +} + + +bool DataSourceManager::addSource(DataSourcePointer source) +{ + if (source.isNull()) return false; + + QString identifier = source->getIdentifier(); + + for (auto src : sources) + { + if (src.isNull()) continue; + + if (src == source || src->getIdentifier() == identifier) + { + qInfo() << "Ignoring duplicate source:" << source->getLabel(); + + return false; + } + } + + sources.push_back(source); + + connect(source.data(), &DataSource::dataChanged, this, &DataSourceManager::onDataChanged); + + emit sourcesChanged(); + + return true; +} + + +/** + * @brief DataSourceManager::addSource adds a DataSource with the given title + * @param label is the label of the DataSource to add + * @return true if the source was added, false if a DataSource with the given title already existed + */ +bool DataSourceManager::addSource(QString source, QString label, QString description) +{ + if (getSourceByLabel(label).isNull()) + { + return addSource(new DataSource(source, label, description)); + } + + // Source with provided label already exists! + return false; +} + + +/** + * @brief DataSourceManager::removeSource removes the DataSource from this DataSourceManager + * @param source is a shared pointer to the DataSource + * @return true if the source was removed, else false + */ +bool DataSourceManager::removeSource(DataSourcePointer source) +{ + for (auto idx = 0; idx < sources.size(); idx++) + { + auto src = sources.at(idx); + + if (src == source) + { + sources.removeAt(idx); + emit sourcesChanged(); + return true; + } + } + + return false; +} + + +/** + * @brief DataSourceManager::removeSourceByIndex removes the DataSource at the specified index + * @param idx - Index of the DataSource to remove + * @param update - If true, emit a "sourcesChanged" event + * @return true if the source was removed, else false (in the case of an invalid index) + */ +bool DataSourceManager::removeSourceByIndex(unsigned int idx, bool update) +{ + if (idx < sources.size()) + { + sources.removeAt(idx); + + if (update) + { + emit sourcesChanged(); + } + + return true; + } + + return false; +} + + +/** + * @brief DataSourceManager::removeSourceByLabel - Remove the first DataSource which matches the specified label + * @param label - QString label + * @param update - If true, emit a "sourcesChanged" event + * @return true if a source matching the provided label was found (and subsequentyly deleted) + */ +bool DataSourceManager::removeSourceByLabel(QString label, bool update) +{ + for (int idx = 0; idx < sources.size(); idx++) + { + auto src = sources.at(idx); + + if (!src.isNull() && src->getLabel() == label) + { + return removeSourceByIndex(idx, update); + } + } + + return false; +} + + +/** + * @brief DataSourceManager::removeAllSources removes all DataSource objects from this DataSourceManager + * @param update - if true, emit a "sourcesChanged" event + */ +void DataSourceManager::removeAllSources(bool update) +{ + while (sources.count() > 0) + { + removeSourceByIndex(0, update); + } + + if (update) + { + emit sourcesChanged(); + } +} + + +bool DataSourceManager::importData(QString filename) +{ + auto registry = PluginRegistry::getInstance(); + auto settings = LumberjackSettings::getInstance(); + + if (filename.isEmpty()) + { + filename = registry->getFilenameForImport(); + } + + // Still empty? No further actions + if (filename.isEmpty()) + { + return false; + } + + QFileInfo fi(filename); + + if (!fi.exists()) + { + qCritical() << "File does not exist:" << filename; + return false; + } + + // Save the last directory information + settings->saveSetting("import", "lastDirectory", fi.absoluteDir().absolutePath()); + + ImportPluginList importers; + + for (auto plugin : registry->ImportPlugins()) + { + if (plugin.isNull()) continue; + + if (plugin->supportsFileType(fi.suffix())) + { + importers.append(plugin); + } + } + + QSharedPointer importer; + + if (importers.length() == 0) + { + // TODO: Error message + return false; + } + else if (importers.length() == 1) + { + importer = importers.first(); + } + else + { + // TODO: Select an importer + // TODO: For now, just take the first one... + importer = importers.first(); + } + + // Create a new instance of the provided importer + DataSource *source = new DataSource( + importer->pluginName(), + fi.fileName(), + fi.absoluteFilePath() + ); + + QStringList errors; + + if (!importer->validateFile(filename, errors)) + { + // TODO: Display errors + + qWarning() << "File is not valid:" << filename; + return false; + } + + importer->setFilename(filename); + + if (!importer->beforeImport()) + { + // TODO: error message? + return false; + } + + errors.clear(); + + bool result = importer->importData(errors); + + if (!result) + { + // TODO: Display errors + + delete source; + return false; + } + + auto seriesList = importer->getDataSeries(); + + if (seriesList.count() == 0) + { + // TODO: Error msg - no data imported + delete source; + return false; + } + + for (auto series : importer->getDataSeries()) + { + source->addSeries(series); + } + + addSource(source); + + return true; +} + + +/** + * @brief DataSourceManager::exportData - Export a set of data series to a file + * @param series + * @param filename + * @return + */ +bool DataSourceManager::exportData(QList &series, QString filename) +{ + auto registry = PluginRegistry::getInstance(); + auto settings = LumberjackSettings::getInstance(); + + if (filename.isEmpty()) + { + filename = registry->getFilenameForExport(); + } + + // Still empty? No further progress + if (filename.isEmpty()) + { + return false; + } + + QFileInfo fi(filename); + + // Save the last directory information + settings->saveSetting("export", "lastDirectory", fi.absoluteDir().absolutePath()); + + ExportPluginList exporters; + + QSharedPointer exporter; + + for (auto plugin : registry->ExportPlugins()) + { + if (plugin.isNull()) continue; + + if (plugin->supportsFileType(fi.suffix())) + { + exporters.append(plugin); + } + } + + if (exporters.length() == 0) + { + // TODO: Error message + return false; + } + else if (exporters.length() == 1) + { + exporter = exporters.first(); + } + else + { + // TODO: select an exporter + // TODO: For now, just take the first one + exporter = exporters.first(); + } + + exporter->setFilename(filename); + + QStringList errors; + + bool result = exporter->exportData(series, errors); + + // TODO: Display errors? + + return result; +} diff --git a/src/data_source_manager.hpp b/src/data_source_manager.hpp new file mode 100644 index 0000000..edf25aa --- /dev/null +++ b/src/data_source_manager.hpp @@ -0,0 +1,81 @@ +#ifndef DATA_SOURCE_MANAGER_HPP +#define DATA_SOURCE_MANAGER_HPP + +#include "data_source.hpp" + +/* + * Data source manager class: + * - Manages all data sources + * - Employs singleton design pattern so can be accessed globally + */ +class DataSourceManager : public QObject +{ + Q_OBJECT + + static DataSourceManager *instance; + +public: + DataSourceManager(); + ~DataSourceManager(); + + static DataSourceManager *getInstance() + { + if (!instance) + { + instance = new DataSourceManager; + } + + return instance; + } + + static void cleanup() + { + if (instance) + { + delete instance; + instance = nullptr; + } + } + +public slots: + + DataSeriesPointer findSeries(QString source_label, QString series_label); + + int getSourceCount(void) const { return sources.size(); } + QStringList getSourceLabels(void) const; + + DataSourcePointer getSourceByIndex(unsigned int idx); + DataSourcePointer getSourceByLabel(QString label); + + bool addSource(DataSourcePointer source); + bool addSource(DataSource* source) { return addSource(DataSourcePointer(source)); } + bool addSource(QString source, QString label, QString description = QString()); + + bool removeSource(DataSourcePointer source); + bool removeSource(DataSource* source) { return removeSource(DataSourcePointer(source)); } + + bool removeSourceByIndex(unsigned int idx, bool update = true); + bool removeSourceByLabel(QString label, bool update = true); + + void removeAllSources(bool update = true); + + // Data import functionality + bool importData(QString filename = QString()); + + // Data export functionality + bool exportData(QList &series, QString filename = QString()); + + void update(void) { emit sourcesChanged(); } + +signals: + void sourcesChanged(); + +protected slots: + void onDataChanged() { emit sourcesChanged(); } + +protected: + QVector sources; +}; + + +#endif // DATA_SOURCE_MANAGER_HPP diff --git a/src/fft_sampler.cpp b/src/fft_sampler.cpp index 09277b8..5e8a0f0 100644 --- a/src/fft_sampler.cpp +++ b/src/fft_sampler.cpp @@ -55,7 +55,7 @@ void FFTCurveUpdater::updateCurveSamples(double t_min, double t_max, unsigned in auto idx_min = series.getIndexForTimestamp(t_min); auto idx_max = series.getIndexForTimestamp(t_max); - if (idx_min < 0) idx_min = 0; + if (idx_min <= 0) idx_min = 0; if (idx_max >= series.size()) idx_max = series.size() - 1; // Recalculate endpoint timestamps diff --git a/src/fft_widget.cpp b/src/fft_widget.cpp index 2ab8f44..537075f 100644 --- a/src/fft_widget.cpp +++ b/src/fft_widget.cpp @@ -44,7 +44,7 @@ void FFTWidget::initAxes() * @brief PlotWidget::generateNewWorker - Create a new curve sampling worker * @return */ -PlotCurveUpdater* FFTWidget::generateNewWorker(QSharedPointer series) +PlotCurveUpdater* FFTWidget::generateNewWorker(DataSeriesPointer series) { return new FFTCurveUpdater(*series); } diff --git a/src/fft_widget.hpp b/src/fft_widget.hpp index 72ee495..0effe59 100644 --- a/src/fft_widget.hpp +++ b/src/fft_widget.hpp @@ -16,7 +16,7 @@ class FFTWidget : public PlotWidget protected: virtual bool isCurveTrackingEnabled(void) const override { return false; } - virtual PlotCurveUpdater* generateNewWorker(QSharedPointer series) override; + virtual PlotCurveUpdater* generateNewWorker(DataSeriesPointer series) override; virtual void resampleCurves(int axis_id = yBoth) override; diff --git a/src/lumberjack_settings.cpp b/src/lumberjack_settings.cpp index d3d0067..99d258d 100644 --- a/src/lumberjack_settings.cpp +++ b/src/lumberjack_settings.cpp @@ -5,7 +5,7 @@ #include -LumberjackSettings *LumberjackSettings::instance = 0; +LumberjackSettings* LumberjackSettings::instance = 0; LumberjackSettings::LumberjackSettings() : settings(getSettingsFile(), QSettings::IniFormat) diff --git a/src/main.cpp b/src/main.cpp index 29a657f..58e39aa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -53,7 +53,7 @@ int main(int argc, char *argv[]) if (!parser.isSet(debugCmdOption)) { // Install custom debug handler - registerLumberjackDebugHandler(); + // registerLumberjackDebugHandler(); } qDebug() << "Starting lumberjack application:" << LUMBERJACK_VERSION_STRING; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index a9ed41f..3c4a03b 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -12,6 +12,7 @@ #include "helpers.hpp" #include "lumberjack_settings.hpp" +#include "data_source_manager.hpp" #include "plot_curve.hpp" #include "data_series.hpp" #include "plot_widget.hpp" @@ -52,14 +53,14 @@ MainWindow::MainWindow(QWidget *parent) loadWorkspaceSettings(); - pluginRegistry.loadPlugins(); + // Load plugins + PluginRegistry::getInstance()->loadPlugins(); } MainWindow::~MainWindow() { - qDebug() << "~MainWindow()"; - + PluginRegistry::cleanup(); DataSourceManager::cleanup(); LumberjackSettings::cleanup(); @@ -108,12 +109,12 @@ void MainWindow::loadDummyData() src = manager->getSourceByLabel("Source A"); - auto series_2 = QSharedPointer(new DataSeries("Series 2")); - auto series_3 = QSharedPointer(new DataSeries("Series 3")); - auto series_4 = QSharedPointer(new DataSeries("Series 4")); - auto series_5 = QSharedPointer(new DataSeries("Series 5")); - auto series_6 = QSharedPointer(new DataSeries("Series 6")); - auto series_7 = QSharedPointer(new DataSeries("Series 7")); + auto series_2 = DataSeriesPointer(new DataSeries("Series 2")); + auto series_3 = DataSeriesPointer(new DataSeries("Series 3")); + auto series_4 = DataSeriesPointer(new DataSeries("Series 4")); + auto series_5 = DataSeriesPointer(new DataSeries("Series 5")); + auto series_6 = DataSeriesPointer(new DataSeries("Series 6")); + auto series_7 = DataSeriesPointer(new DataSeries("Series 7")); for (double t = 0; t < 100; t += 0.0001) { @@ -249,7 +250,7 @@ void MainWindow::initSignalsSlots() connect(tree, &DataViewTree::onSeriesRemoved, this, &MainWindow::seriesRemoved); } - connect(&dataView, &DataviewWidget::fileDropped, this, &MainWindow::loadDroppedFile); + connect(&dataView, &DataviewWidget::fileDropped, this, &MainWindow::loadDataFromFile); // Timeline view connect(&timelineView, &TimelineWidget::timeUpdated, this, &MainWindow::onTimescaleChanged); @@ -279,7 +280,7 @@ void MainWindow::onTimescaleChanged(const QwtInterval &viewInterval) { auto source = sender(); - QList> seriesList; + QList seriesList; // Update the timescale on other plots for (auto plot : plots) @@ -343,7 +344,7 @@ void MainWindow::updateCursorPos(double t, double y1, double y2) */ void MainWindow::showPluginsInfo(void) { - PluginsDialog dlg(pluginRegistry, this); + PluginsDialog dlg(this); dlg.exec(); } @@ -374,6 +375,7 @@ void MainWindow::toggleDebugView() else { QDockWidget *dock = new QDockWidget(tr("Debug View"), this); + dock->setObjectName("debug-view"); dock->setAllowedAreas(Qt::AllDockWidgetAreas); dock->setWidget(&debugWidget); @@ -383,16 +385,6 @@ void MainWindow::toggleDebugView() } -/* - * Callback for loading a data file which is dropped into the window - */ -void MainWindow::loadDroppedFile(QString filename) -{ - qDebug() << "MainWindow::loadDroppedFile" << filename; - loadDataFromFile(filename); -} - - /** * @brief MainWindow::loadDataFromFile - Load data from the provided file @@ -400,102 +392,8 @@ void MainWindow::loadDroppedFile(QString filename) */ void MainWindow::loadDataFromFile(QString filename) { - qDebug() << "loadDataFromFile:" << filename; - - auto *settings = LumberjackSettings::getInstance(); - - // Record the directory this file was loaded from - QFileInfo fi(filename); - - if (!fi.exists()) - { - // TODO - show error message - return; - } - - settings->saveSetting("import", "lastDirectory", fi.absoluteDir().absolutePath()); - - // Find a matching plugin - ImportPluginList importers; - - QSharedPointer importer; - - for (auto plugin : pluginRegistry.ImportPlugins()) - { - if (plugin.isNull()) continue; - - if (plugin->supportsFileType(fi.suffix())) - { - importers.append(plugin); - } - } - - if (importers.length() == 1) - { - importer = importers.first(); - } - else if (importers.length() == 0) - { - // TODO: Select an importer - // TODO: For now, just take the first one... - importer = importers.first(); - } - else - { - // TODO: Select an importer - // TODO: For now, just take the first one... - importer = importers.first(); - } - - // Create a new instance of the provided importer - DataSource *source = new DataSource(importer->pluginName(), importer->pluginDescription()); - - QStringList errors; - - - if (!importer->validateFile(filename, errors)) - { - // TODO: error message? - return; - } - - importer->setFilename(filename); - - if (!importer->beforeLoadData()) - { - // TODO: error message? - return; - } - - errors.clear(); - - bool result = importer->loadDataFile(errors); - - if (!result) - { - // TODO: Display errors - - delete source; - return; - } - - auto seriesList = importer->getDataSeries(); - - if (seriesList.count() == 0) - { - // TODO: Error msg - no data imported - delete source; - return; - } - - for (auto series : importer->getDataSeries()) - { - source->addSeries(series); - } - - DataSourceManager::getInstance()->addSource(source); - - // TODO: Success message? + auto manager = DataSourceManager::getInstance(); + manager->importData(filename); } @@ -504,60 +402,7 @@ void MainWindow::loadDataFromFile(QString filename) */ void MainWindow::importData() { - auto *settings = LumberjackSettings::getInstance(); - - qDebug() << "MainWindow::importData"; - - // Assemble set of supported file types - QStringList supportedFileTypes; - - QStringList filePatterns; - - for (QSharedPointer plugin : pluginRegistry.ImportPlugins()) - { - if (plugin.isNull()) continue; - - filePatterns.append(plugin->fileFilter()); - } - - filePatterns.append("Any files (*)"); - - // Load a file - QFileDialog dialog(this); - - dialog.setWindowTitle(tr("Import Data from File")); - - QString lastDir = settings->loadSetting("import", "lastDirectory", QString()).toString(); - - if (!lastDir.isEmpty()) - { - dialog.setDirectory(lastDir); - } - - dialog.setFileMode(QFileDialog::ExistingFile); - dialog.setNameFilters(filePatterns); - dialog.setViewMode(QFileDialog::Detail); - - int result = dialog.exec(); - - if (result != QDialog::Accepted) - { - // User cancelled the import process - return; - } - - // Determine which plugin loaded the data - QString filter = dialog.selectedNameFilter(); - QStringList files = dialog.selectedFiles(); - - if (filter.isEmpty() || files.length() != 1) - { - return; - } - - QString filename = files.first(); - - loadDataFromFile(filename); + loadDataFromFile(); } @@ -577,7 +422,7 @@ void MainWindow::hideDockedWidget(QWidget *widget) /* * Callback when a DataSeries is removed from the available graphs */ -void MainWindow::seriesRemoved(QSharedPointer series) +void MainWindow::seriesRemoved(DataSeriesPointer series) { if (series.isNull()) return; @@ -597,11 +442,10 @@ void MainWindow::addPlot() { static int plotIndex = 1; - qDebug() << "MainWindow::addPlot()"; - PlotWidget *plot = new PlotWidget(); QDockWidget *dock = new QDockWidget(tr("Plot") + QString(" ") + QString::number(plotIndex), this); + dock->setObjectName("plot-" + QString::number(plotIndex)); dock->setAllowedAreas(Qt::AllDockWidgetAreas); plot->setParent(dock); dock->setWidget(plot); @@ -616,7 +460,7 @@ void MainWindow::addPlot() connect(plot, &PlotWidget::viewChanged, this, &MainWindow::onTimescaleChanged); connect(plot, &PlotWidget::viewChanged, &timelineView, &TimelineWidget::updateViewLimits); connect(plot, &PlotWidget::timestampLimitsChanged, &timelineView, &TimelineWidget::updateTimeLimits); - connect(plot, &PlotWidget::fileDropped, this, &MainWindow::loadDroppedFile); + connect(plot, &PlotWidget::fileDropped, this, &MainWindow::loadDataFromFile); plots.append(QSharedPointer(plot)); @@ -646,6 +490,7 @@ void MainWindow::toggleFftView(void) else { QDockWidget* dock = new QDockWidget(tr("FFT View"), this); + dock->setObjectName("fft-view"); dock->setAllowedAreas(Qt::AllDockWidgetAreas); dock->setWidget(&fftView); @@ -672,6 +517,7 @@ void MainWindow::toggleDataView(void) else { QDockWidget* dock = new QDockWidget(tr("Data View"), this); + dock->setObjectName("data-view"); dock->setAllowedAreas(Qt::AllDockWidgetAreas); dock->setWidget(&dataView); @@ -698,6 +544,7 @@ void MainWindow::toggleTimelineView(void) else { QDockWidget* dock = new QDockWidget(tr("Timeline"), this); + dock->setObjectName("timeline-view"); dock->setAllowedAreas(Qt::TopDockWidgetArea | Qt::BottomDockWidgetArea); dock->setWidget(&timelineView); @@ -724,6 +571,7 @@ void MainWindow::toggleStatisticsView(void) else { QDockWidget *dock = new QDockWidget(tr("Stats View"), this); + dock->setObjectName("stats-view"); dock->setAllowedAreas(Qt::AllDockWidgetAreas); dock->setWidget(&statsView); diff --git a/src/mainwindow.h b/src/mainwindow.h index ac9be18..40a7abb 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -12,7 +12,6 @@ #include "stats_widget.hpp" #include "dataview_widget.hpp" #include "timeline_widget.hpp" -#include "plugin_registry.hpp" QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } @@ -32,7 +31,7 @@ public slots: void onTimescaleChanged(const QwtInterval &view); void updateCursorPos(double t, double y1, double y2); - void loadDataFromFile(QString filename); + void loadDataFromFile(QString filename = QString()); protected: void initMenus(void); @@ -51,7 +50,6 @@ protected slots: void showPluginsInfo(void); void importData(void); - void loadDroppedFile(QString filename); void toggleDebugView(void); void toggleDataView(void); @@ -64,7 +62,7 @@ protected slots: void hideDockedWidget(QWidget *widget); - void seriesRemoved(QSharedPointer series); + void seriesRemoved(DataSeriesPointer series); private: Ui::MainWindow *ui; @@ -81,8 +79,5 @@ protected slots: FFTWidget fftView; DebugWidget debugWidget; - - PluginRegistry pluginRegistry; - }; #endif // MAINWINDOW_H diff --git a/src/plot_curve.cpp b/src/plot_curve.cpp index 7206b79..7e1a00b 100644 --- a/src/plot_curve.cpp +++ b/src/plot_curve.cpp @@ -13,7 +13,7 @@ * @param s - the source data series * @param updater - the worker which resamples the data */ -PlotCurve::PlotCurve(QSharedPointer s, PlotCurveUpdater *updater) : QwtPlotCurve(), series(s) +PlotCurve::PlotCurve(DataSeriesPointer s, PlotCurveUpdater *updater) : QwtPlotCurve(), series(s) { // Create a new worker thread @@ -31,11 +31,7 @@ PlotCurve::PlotCurve(QSharedPointer s, PlotCurveUpdater *updater) : connect(&(*series), &DataSeries::styleUpdated, this, &PlotCurve::updateLineStyle); connect(&(*series), &DataSeries::styleUpdated, this, &PlotCurve::updateLabel); -#ifdef CI_UNIT_TEST - qDebug() << "Skipping GUI steps for unit testing"; -#else updateLineStyle(); -#endif } worker->moveToThread(&workerThread); @@ -45,16 +41,12 @@ PlotCurve::PlotCurve(QSharedPointer s, PlotCurveUpdater *updater) : PlotCurve::~PlotCurve() { - qDebug() << "Deleting PlotCurve"; - // Wait for the resampling thread to complete workerThread.quit(); workerThread.wait(); // Delete the worker thread delete worker; - - qDebug() << "~PlotCurve() complete"; } @@ -67,17 +59,17 @@ void PlotCurve::onDataResampled(const QVector &t_data, const QVectorupdateCurveSamples(t_min, t_max, n_pixels); + qWarning() << "PlotCurve::resampleData:" << "series is null"; } - else if (series.isNull()) + else if (worker == nullptr) { - qDebug() << "resampleData:" << "series is null"; + qWarning() << "PlotCurve::resampleData:" << "worker is null"; } - else if (worker == nullptr) + else { - qDebug() << "resampleData:" << "worker is null"; + worker->updateCurveSamples(t_min, t_max, n_pixels); } } diff --git a/src/plot_curve.hpp b/src/plot_curve.hpp index 9f9a29a..94c49f6 100644 --- a/src/plot_curve.hpp +++ b/src/plot_curve.hpp @@ -24,10 +24,10 @@ class PlotCurve : public QObject, public QwtPlotCurve Q_OBJECT public: - PlotCurve(QSharedPointer series, PlotCurveUpdater* updater); - PlotCurve(DataSeries* series, PlotCurveUpdater *updater) : PlotCurve(QSharedPointer(series), updater) {} + PlotCurve(DataSeriesPointer series, PlotCurveUpdater* updater); + PlotCurve(DataSeries* series, PlotCurveUpdater *updater) : PlotCurve(DataSeriesPointer(series), updater) {} - QSharedPointer getDataSeries(void) { return series; } + DataSeriesPointer getDataSeries(void) { return series; } virtual ~PlotCurve(); @@ -42,7 +42,7 @@ protected slots: void onDataResampled(const QVector &t_data, const QVector &y_data); protected: - QSharedPointer series; + DataSeriesPointer series; PlotCurveUpdater *worker = nullptr; diff --git a/src/plot_widget.cpp b/src/plot_widget.cpp index c667e3f..82a6a9c 100644 --- a/src/plot_widget.cpp +++ b/src/plot_widget.cpp @@ -14,8 +14,9 @@ #include "axis_edit_dialog.hpp" #include "series_editor_dialog.hpp" -#include "data_source.hpp" #include "plot_widget.hpp" + +#include "data_source_manager.hpp" #include "lumberjack_settings.hpp" @@ -337,11 +338,15 @@ void PlotWidget::onContextMenu(const QPoint &pos) // Data menu QMenu *dataMenu = new QMenu(tr("Data"), &menu); + QAction *exportData = dataMenu->addAction(tr("Export Data")); + dataMenu->addSeparator(); QAction *imageToClipboard = dataMenu->addAction(tr("Image to Clipboard")); QAction *imageToFile = dataMenu->addAction(tr("Image to File")); dataMenu->addSeparator(); QAction *clearAll = dataMenu->addAction(tr("Clear All")); + exportData->setEnabled(curves.count() > 0); + menu.addMenu(dataMenu); // Curve menu @@ -427,6 +432,10 @@ void PlotWidget::onContextMenu(const QPoint &pos) { yGridEnable(!isYGridEnabled()); } + else if (action == exportData) + { + exportDataToFile(); + } else if (action == imageToClipboard) { saveImageToClipboard(); @@ -535,6 +544,24 @@ void PlotWidget::selectBackgroundColor() } +/** + * @brief PlotWidget::exportDataToFile - export data to a file + */ +void PlotWidget::exportDataToFile() +{ + auto manager = DataSourceManager::getInstance(); + + QList dataSeries; + + for (auto curve : curves) + { + dataSeries.append(curve->getDataSeries()); + } + + manager->exportData(dataSeries); +} + + /** * @brief PlotWidget::saveImageToClipboard - copy the current plot view to clipboard */ @@ -648,6 +675,11 @@ void PlotWidget::dropEvent(QDropEvent *event) auto *mime = event->mimeData(); auto *manager = DataSourceManager::getInstance(); + if (!mime || !manager) + { + return; + } + // DataSeries is dropped onto this PlotWidget if (mime->hasFormat("source") && mime->hasFormat("series")) { @@ -1395,7 +1427,7 @@ void PlotWidget::updateCursorShape(QMouseEvent *event) * @param axis_id - axis ID (either QwtPlot::yLeft or QwtPlot::yRight) * @return true if the series was added */ -bool PlotWidget::addSeries(QSharedPointer series, int axis_id) +bool PlotWidget::addSeries(DataSeriesPointer series, int axis_id) { if (series.isNull()) return false; @@ -1438,7 +1470,7 @@ bool PlotWidget::addSeries(QSharedPointer series, int axis_id) * @param series - DataSeries pointer * @return true if the DataSeries existed and was removed */ -bool PlotWidget::removeSeries(QSharedPointer series) +bool PlotWidget::removeSeries(DataSeriesPointer series) { for (int idx = 0; idx < curves.size(); idx++) { @@ -1677,7 +1709,7 @@ int PlotWidget::getHorizontalPixels() const * @brief PlotWidget::generateNewWorker - Create a new curve sampling worker * @return */ -PlotCurveUpdater* PlotWidget::generateNewWorker(QSharedPointer series) +PlotCurveUpdater* PlotWidget::generateNewWorker(DataSeriesPointer series) { return new PlotCurveUpdater(*series); } diff --git a/src/plot_widget.hpp b/src/plot_widget.hpp index 82fbf45..be84711 100644 --- a/src/plot_widget.hpp +++ b/src/plot_widget.hpp @@ -52,11 +52,11 @@ class PlotWidget : public QwtPlot public slots: int getHorizontalPixels(void) const; - bool addSeries(QSharedPointer series, int axis_id = QwtPlot::yLeft); - bool addSeries(DataSeries *series) { return addSeries(QSharedPointer(series)); } + bool addSeries(DataSeriesPointer series, int axis_id = QwtPlot::yLeft); + bool addSeries(DataSeries *series) { return addSeries(DataSeriesPointer(series)); } - bool removeSeries(QSharedPointer series); - bool removeSeries(DataSeries *series) { return removeSeries(QSharedPointer(series)); } + bool removeSeries(DataSeriesPointer series); + bool removeSeries(DataSeries *series) { return removeSeries(DataSeriesPointer(series)); } bool removeSeries(QString label); void removeAllSeries(); @@ -79,6 +79,8 @@ public slots: void selectBackgroundColor(); + void exportDataToFile(); + void saveImageToClipboard(); void saveImageToFile(); @@ -114,7 +116,7 @@ protected slots: void initCrosshairs(void); void initGrid(void); - virtual PlotCurveUpdater* generateNewWorker(QSharedPointer series); + virtual PlotCurveUpdater* generateNewWorker(DataSeriesPointer series); virtual void resampleCurves(int axis_id = yBoth); diff --git a/src/plugins/plugin_exporter.cpp b/src/plugins/plugin_exporter.cpp new file mode 100644 index 0000000..f234768 --- /dev/null +++ b/src/plugins/plugin_exporter.cpp @@ -0,0 +1,40 @@ +#include "plugin_exporter.hpp" + + +bool ExportPlugin::supportsFileType(QString fileType) const +{ + fileType = fileType.replace(".", "").toLower(); + + for (QString sft : supportedFileTypes()) + { + if (sft.replace(".", "").toLower() == fileType) + { + return true; + } + } + + return false; +} + + +QString ExportPlugin::fileFilter(void) const +{ + QString filter = pluginName(); + + filter += " ("; + + QStringList fileFilters; + + for (QString ft : supportedFileTypes()) + { + if (!ft.startsWith(".")) ft.prepend("."); + ft.prepend("*"); + + fileFilters.append(ft); + } + + filter += fileFilters.join(" "); + filter += ")"; + + return filter; +} diff --git a/src/plugins/plugin_exporter.hpp b/src/plugins/plugin_exporter.hpp index e3577e2..058009e 100644 --- a/src/plugins/plugin_exporter.hpp +++ b/src/plugins/plugin_exporter.hpp @@ -4,6 +4,7 @@ #include #include "plugin_base.hpp" +#include "data_series.hpp" #define ExporterInterface_iid "org.lumberjack.plugins.ExportPlugin/1.0" @@ -19,12 +20,28 @@ class ExportPlugin : public PluginBase // Return a list of the support file types e.g. ['csv', 'tsv'] virtual QStringList supportedFileTypes(void) const = 0; - // TODO: Export data function + // Optional function called before data export + // Return False to cancel the data export process + virtual bool beforeExport(void) { return true; } + + // Export data to the provided filename + virtual bool exportData(QList &series, QStringList &errors) = 0; virtual QString pluginIID(void) const override { return QString(ExporterInterface_iid); } + + QString fileFilter(void) const; + + bool supportsFileType(QString fileType) const; + + void setFilename(QString filename) { m_filename = filename; } + QString getFilename(void) const { return m_filename; } + +protected: + // Stored filename, destination of exported data + QString m_filename; }; typedef QList> ExportPluginList; diff --git a/src/plugins/plugin_importer.hpp b/src/plugins/plugin_importer.hpp index c7f9b46..41d051f 100644 --- a/src/plugins/plugin_importer.hpp +++ b/src/plugins/plugin_importer.hpp @@ -27,15 +27,15 @@ class ImportPlugin : public PluginBase // but this can be extended by the plugin virtual bool validateFile(QString filename, QStringList &errors) const; - // Optional function called before data loading - // Return False to cancel the data import - virtual bool beforeLoadData() { return true; } + // Optional function called before data import + // Return False to cancel the data import process + virtual bool beforeImport() { return true; } // Load data from the provided filename - virtual bool loadDataFile(QStringList &errors) = 0; + virtual bool importData(QStringList &errors) = 0; // After import, plugin must return a list of DataSeries objects - virtual QList> getDataSeries(void) const = 0; + virtual QList getDataSeries(void) const = 0; // Return the IID string virtual QString pluginIID(void) const override diff --git a/src/plugins/plugin_registry.cpp b/src/plugins/plugin_registry.cpp index dabfe2e..16e18a0 100644 --- a/src/plugins/plugin_registry.cpp +++ b/src/plugins/plugin_registry.cpp @@ -1,13 +1,18 @@ #include #include +#include +#include #include #include #include "plugin_registry.hpp" +#include "lumberjack_settings.hpp" + +PluginRegistry* PluginRegistry::instance = 0; -PluginRegistry::PluginRegistry(QObject *parent) : QObject(parent) -{ +PluginRegistry::PluginRegistry() : QObject() +{ } @@ -56,10 +61,6 @@ void PluginRegistry::loadPlugins() } } } - - qDebug() << "Loading plugins:"; - qDebug() << "Importer:" << m_ImportPlugins.count(); - qDebug() << "Exporter:" << m_ExportPlugins.count(); } @@ -114,3 +115,113 @@ bool PluginRegistry::loadFilterPlugin(QObject *instance) return false; } + + +/** + * @brief PluginRegistry::getFilenameForImport - Select a file for importing + * @return + */ +QString PluginRegistry::getFilenameForImport(void) const +{ + auto settings = LumberjackSettings::getInstance(); + + QStringList filePatterns; + + for (QSharedPointer plugin : m_ImportPlugins) + { + if (plugin.isNull()) continue; + + filePatterns.append(plugin->fileFilter()); + } + + filePatterns.append("Any files (*)"); + + QFileDialog dialog; + + dialog.setWindowTitle(tr("Import Data from File")); + + QString lastDir = settings->loadSetting("import", "lastDirectory", QString()).toString(); + + if (!lastDir.isEmpty()) + { + dialog.setDirectory(lastDir); + } + + dialog.setFileMode(QFileDialog::ExistingFile); + dialog.setNameFilters(filePatterns); + dialog.setViewMode(QFileDialog::Detail); + + int result = dialog.exec(); + + + if (result != QDialog::Accepted) + { + // User cancelled the import process + return QString(); + } + + // Determine which plugin loaded the data + QString filter = dialog.selectedNameFilter(); + QStringList files = dialog.selectedFiles(); + + QString filename; + + if (!filter.isEmpty() && files.length() >= 1) + { + filename = files.first(); + } + + return filename; +} + + +QString PluginRegistry::getFilenameForExport(void) const +{ + auto settings = LumberjackSettings::getInstance(); + + QStringList filePatterns; + + for (QSharedPointer plugin : m_ExportPlugins) + { + if (plugin.isNull()) continue; + + filePatterns.append(plugin->fileFilter()); + } + + filePatterns.append("Any Files (*)"); + + QFileDialog dialog; + + dialog.setWindowTitle(tr("Export Data to File")); + + QString lastDir = settings->loadSetting("export", "lastDirectory", QString()).toString(); + + if (!lastDir.isEmpty()) + { + dialog.setDirectory(lastDir); + } + + dialog.setFileMode(QFileDialog::AnyFile); + dialog.setNameFilters(filePatterns); + dialog.setViewMode(QFileDialog::Detail); + dialog.setLabelText(QFileDialog::DialogLabel::Accept, tr("Export")); + + int result = dialog.exec(); + + if (result != QDialog::Accepted) + { + return QString(); + } + + QString filter = dialog.selectedNameFilter(); + QStringList files = dialog.selectedFiles(); + + QString filename; + + if (!filter.isEmpty() && files.length() >= 1) + { + filename = files.first(); + } + + return filename; +} diff --git a/src/plugins/plugin_registry.hpp b/src/plugins/plugin_registry.hpp index 73a5b4f..97803f3 100644 --- a/src/plugins/plugin_registry.hpp +++ b/src/plugins/plugin_registry.hpp @@ -1,8 +1,6 @@ #ifndef PLUGIN_REGISTRY_HPP #define PLUGIN_REGISTRY_HPP -#include - #include "plugin_filter.hpp" #include "plugin_importer.hpp" #include "plugin_exporter.hpp" @@ -13,10 +11,32 @@ class PluginRegistry : public QObject { Q_OBJECT + static PluginRegistry* instance; + public: - PluginRegistry(QObject *parent = nullptr); + PluginRegistry(); virtual ~PluginRegistry(); + static PluginRegistry* getInstance() + { + if (!instance) + { + instance = new PluginRegistry(); + } + + return instance; + } + + static void cleanup() + { + if (instance) + { + instance->clearRegistry(); + delete instance; + instance = nullptr; + } + } + void loadPlugins(void); void clearRegistry(void); @@ -24,6 +44,9 @@ class PluginRegistry : public QObject const ExportPluginList& ExportPlugins(void) { return m_ExportPlugins; } const FilterPluginList& FilterPlugins(void) { return m_FilterPlugins; } + QString getFilenameForImport(void) const; + QString getFilenameForExport(void) const; + protected: bool loadImportPlugin(QObject *instance); diff --git a/src/widgets/datatable_widget.cpp b/src/widgets/datatable_widget.cpp index 5140bdc..0042d00 100644 --- a/src/widgets/datatable_widget.cpp +++ b/src/widgets/datatable_widget.cpp @@ -1,7 +1,7 @@ #include "datatable_widget.hpp" -DataSeriesTableModel::DataSeriesTableModel(QSharedPointer series, QObject* parent) : QAbstractTableModel(parent) +DataSeriesTableModel::DataSeriesTableModel(DataSeriesPointer series, QObject* parent) : QAbstractTableModel(parent) { this->series = series; } @@ -62,7 +62,7 @@ QVariant DataSeriesTableModel::data(const QModelIndex &index, int role) const } -DataSeriesTableView::DataSeriesTableView(QSharedPointer series, QWidget *parent) : +DataSeriesTableView::DataSeriesTableView(DataSeriesPointer series, QWidget *parent) : QTableView(parent), model(series) { diff --git a/src/widgets/datatable_widget.hpp b/src/widgets/datatable_widget.hpp index 9756d54..5f66e5a 100644 --- a/src/widgets/datatable_widget.hpp +++ b/src/widgets/datatable_widget.hpp @@ -12,7 +12,7 @@ class DataSeriesTableModel : public QAbstractTableModel Q_OBJECT public: - DataSeriesTableModel(QSharedPointer series, QObject *parent = nullptr); + DataSeriesTableModel(DataSeriesPointer series, QObject *parent = nullptr); int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; @@ -20,7 +20,7 @@ class DataSeriesTableModel : public QAbstractTableModel QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; protected: - QSharedPointer series; + DataSeriesPointer series; }; @@ -29,7 +29,7 @@ class DataSeriesTableView : public QTableView Q_OBJECT public: - DataSeriesTableView(QSharedPointer series, QWidget *parent = nullptr); + DataSeriesTableView(DataSeriesPointer series, QWidget *parent = nullptr); virtual ~DataSeriesTableView(); protected: diff --git a/src/widgets/dataview_tree.cpp b/src/widgets/dataview_tree.cpp index b683172..7f2890f 100644 --- a/src/widgets/dataview_tree.cpp +++ b/src/widgets/dataview_tree.cpp @@ -6,8 +6,8 @@ #include "datatable_widget.hpp" #include "series_editor_dialog.hpp" -#include "data_source.hpp" #include "dataview_tree.hpp" +#include "data_source_manager.hpp" DataViewTree::DataViewTree(QWidget *parent) : QTreeWidget(parent) @@ -64,7 +64,7 @@ void DataViewTree::setupTree() /* * Callback function to edit a particular DataSeries. */ -void DataViewTree::editDataSeries(QSharedPointer series) +void DataViewTree::editDataSeries(DataSeriesPointer series) { if (series.isNull()) { @@ -123,6 +123,9 @@ void DataViewTree::onContextMenu(const QPoint &pos) return; } + // Export series + QAction *exportSeries = new QAction(tr("Export Series"), &menu); + // Edit series QAction *editSeries = new QAction(tr("Edit Series"), &menu); @@ -132,6 +135,8 @@ void DataViewTree::onContextMenu(const QPoint &pos) // Delete series QAction *deleteSeries = new QAction(tr("Delete Series"), &menu); + menu.addAction(exportSeries); + menu.addSeparator(); menu.addAction(editSeries); menu.addAction(viewSeriesData); menu.addSeparator(); @@ -139,7 +144,14 @@ void DataViewTree::onContextMenu(const QPoint &pos) QAction *action = menu.exec(mapToGlobal(pos)); - if (action == editSeries) + if (action == exportSeries) + { + QList dataSeries; + dataSeries << series; + + DataSourceManager::getInstance()->exportData(dataSeries); + } + else if (action == editSeries) { editDataSeries(series); } @@ -238,17 +250,20 @@ int DataViewTree::refresh(QString filters) int series_count = 0; - for (QString source_label : manager->getSourceLabels()) + for (int ii = 0; ii < manager->getSourceCount(); ii++) { - auto source = manager->getSourceByLabel(source_label); - - QStringList labels = source->getSeriesLabels(filters); + auto source = manager->getSourceByIndex(ii); if (source.isNull()) continue; + QStringList labels = source->getSeriesLabels(filters); + + // Add a header item for this "data source" QTreeWidgetItem *item = new QTreeWidgetItem(); - item->setText(1, source_label); + QString uid = source->getIdentifier(); + + item->setText(1, source->getLabel()); // Embolden text for "source" QFont font = item->font(1); diff --git a/src/widgets/dataview_tree.hpp b/src/widgets/dataview_tree.hpp index 19a2d56..1a6cfa6 100644 --- a/src/widgets/dataview_tree.hpp +++ b/src/widgets/dataview_tree.hpp @@ -20,13 +20,13 @@ public slots: void onContextMenu(const QPoint &pos); signals: - void onSeriesRemoved(QSharedPointer series); + void onSeriesRemoved(DataSeriesPointer series); protected: virtual void startDrag(Qt::DropActions supported_actions) override; void setupTree(); - void editDataSeries(QSharedPointer series); + void editDataSeries(DataSeriesPointer series); QString filterString; }; diff --git a/src/widgets/dataview_widget.cpp b/src/widgets/dataview_widget.cpp index 14594a6..84181ef 100644 --- a/src/widgets/dataview_widget.cpp +++ b/src/widgets/dataview_widget.cpp @@ -2,6 +2,7 @@ #include #include +#include "data_source_manager.hpp" #include "dataview_widget.hpp" diff --git a/src/widgets/plugins_dialog.cpp b/src/widgets/plugins_dialog.cpp index b3987ff..8a09dc4 100644 --- a/src/widgets/plugins_dialog.cpp +++ b/src/widgets/plugins_dialog.cpp @@ -1,7 +1,8 @@ #include "plugins_dialog.hpp" +#include "plugin_registry.hpp" -PluginsDialog::PluginsDialog(PluginRegistry &r, QWidget *parent) : QDialog(parent), registry(r) +PluginsDialog::PluginsDialog(QWidget *parent) : QDialog(parent) { ui.setupUi(this); @@ -67,24 +68,26 @@ void PluginsDialog::loadPluginsTable(const PluginList &plugins) void PluginsDialog::selectPluginType(int idx) { + auto* registry = PluginRegistry::getInstance(); + PluginList plugins; switch (idx) { case 1: // Importer plugins - for (auto plugin : registry.ImportPlugins()) + for (auto plugin : registry->ImportPlugins()) { plugins.append(plugin); } break; case 2: // Exporter plugins - for (auto plugin : registry.ExportPlugins()) + for (auto plugin : registry->ExportPlugins()) { plugins.append(plugin); } break; case 3: // Filter plugins - for (auto plugin : registry.FilterPlugins()) + for (auto plugin : registry->FilterPlugins()) { plugins.append(plugin); } diff --git a/src/widgets/plugins_dialog.hpp b/src/widgets/plugins_dialog.hpp index aa4ba15..1d29dab 100644 --- a/src/widgets/plugins_dialog.hpp +++ b/src/widgets/plugins_dialog.hpp @@ -3,8 +3,7 @@ #include -#include "plugin_registry.hpp" - +#include "plugin_base.hpp" #include "ui_plugins_dialog.h" class PluginsDialog : public QDialog @@ -12,7 +11,7 @@ class PluginsDialog : public QDialog Q_OBJECT public: - PluginsDialog(PluginRegistry &r, QWidget *parent = nullptr); + PluginsDialog(QWidget *parent = nullptr); public slots: void selectPluginType(int idx); @@ -23,8 +22,6 @@ public slots: void initPluginsTable(void); void loadPluginsTable(const PluginList &plugins); - PluginRegistry ®istry; - }; #endif // PLUGINS_DIALOG_HPP diff --git a/src/widgets/series_editor_dialog.cpp b/src/widgets/series_editor_dialog.cpp index 4319d75..1185468 100644 --- a/src/widgets/series_editor_dialog.cpp +++ b/src/widgets/series_editor_dialog.cpp @@ -5,7 +5,7 @@ #include "series_editor_dialog.hpp" -SeriesEditorDialog::SeriesEditorDialog(QSharedPointer s, QWidget *parent) : QDialog(parent), series(s) +SeriesEditorDialog::SeriesEditorDialog(DataSeriesPointer s, QWidget *parent) : QDialog(parent), series(s) { ui.setupUi(this); diff --git a/src/widgets/series_editor_dialog.hpp b/src/widgets/series_editor_dialog.hpp index aa239d7..0d2dc9c 100644 --- a/src/widgets/series_editor_dialog.hpp +++ b/src/widgets/series_editor_dialog.hpp @@ -13,7 +13,7 @@ class SeriesEditorDialog : public QDialog Q_OBJECT public: - SeriesEditorDialog(QSharedPointer series, QWidget *parent = nullptr); + SeriesEditorDialog(DataSeriesPointer series, QWidget *parent = nullptr); virtual ~SeriesEditorDialog(); public slots: @@ -25,7 +25,7 @@ public slots: protected: Ui::series_editor_form ui; - QSharedPointer series; + DataSeriesPointer series; QColor color; diff --git a/src/widgets/stats_widget.cpp b/src/widgets/stats_widget.cpp index 2c0ea78..a3efa7b 100644 --- a/src/widgets/stats_widget.cpp +++ b/src/widgets/stats_widget.cpp @@ -32,7 +32,7 @@ void StatsWidget::initTable() } -void StatsWidget::updateStats(const QList> &seriesList, const QwtInterval &interval) +void StatsWidget::updateStats(const QList &seriesList, const QwtInterval &interval) { auto* table = ui.statsTable; diff --git a/src/widgets/stats_widget.hpp b/src/widgets/stats_widget.hpp index a8e65c2..00dbbcf 100644 --- a/src/widgets/stats_widget.hpp +++ b/src/widgets/stats_widget.hpp @@ -18,7 +18,7 @@ class StatsWidget : public QWidget StatsWidget(QWidget *parent = nullptr); public slots: - void updateStats(const QList> &series, const QwtInterval &interval); + void updateStats(const QList &series, const QwtInterval &interval); protected: Ui::stats_form ui;