diff --git a/.gitmodules b/.gitmodules index aeaaeb50ec5b3705b1bdf9fb250f6ce5dac6664a..318e50e507845db1f06a7e49415d0680acafafce 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "radix"] - path = submodules/radix - url = https://code.ornl.gov/jap/radix.git [submodule "TriBITS"] path = submodules/TriBITS url = https://github.com/lefebvre/TriBITS.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 6253317af1b2ac21cd4b188d362ef5bde87d2786..6277458bf414657481c1aba59338a368a059c3c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,8 +29,8 @@ IF (NOT TRIBITS_PROCESSING_PACKAGE) IF(DEBUG_OUTPUT) ADD_DEFINITIONS("-DDEBUG_OUTPUT=${DEBUG_OUTPUT}") ENDIF() - IF(RADIX_DBC) - ADD_DEFINITIONS("-DRADIX_DBC=${RADIX_DBC}") + IF(QCP_DBC) + ADD_DEFINITIONS("-QCP_DBC=${QCP_DBC}") ENDIF() # # For windows with BUILD_SHARED_LIBS we must use CMAKE_RUNTIME_OUTPUT_DIRECTORY @@ -40,7 +40,7 @@ IF (NOT TRIBITS_PROCESSING_PACKAGE) SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/cmake_runtime_output" CACHE STRING "") ENDIF() ENDIF() - + SET(USE_QT5 ON CACHE BOOL "") TRIBITS_PROJECT() ELSE() # This CMakeLists.txt file is being processed as the TriBITS package file. diff --git a/PackagesList.cmake b/PackagesList.cmake index 2a30980d01cdf0ca7f5444fdca89ffec49ad928e..8a9dd464d4d2da59cb9a0b3d2d190d662faefa93 100644 --- a/PackagesList.cmake +++ b/PackagesList.cmake @@ -13,14 +13,12 @@ TRIBITS_REPOSITORY_DEFINE_PACKAGES( googletest submodules/googletest/googletest PT testframework submodules/testframework PT - radix submodules/radix PT qcp . PT ) TRIBITS_ALLOW_MISSING_EXTERNAL_PACKAGES( googletest testframework - radix ) ##---------------------------------------------------------------------------## diff --git a/README.md b/README.md index d3511aeb5be2a516770d77b9ceb1bc4aefae6693..bc9758ee9e034af7cad975d3e1e69bab64e9f59a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # Getting Started * You will need to save your ssh-key in [code.ornl.gov](https://code.ornl.gov/profile/keys). * Clone qcp -`git clone git@code.ornl.gov:jap/qcp.git ~/qcp` +`git clone git@code.ornl.gov:nsm/qcp.git ~/qcp` * Initialize and update git submodules `git submodule init && git submodule update` * Change directory into qcp `cd ~/qcp` diff --git a/qcpcontext/CMakeLists.txt b/qcpcontext/CMakeLists.txt index be1bc49dd216a38ebc643315822a63436226fa94..2850eb7545a63d7546ac0479a805cd2fde981c20 100644 --- a/qcpcontext/CMakeLists.txt +++ b/qcpcontext/CMakeLists.txt @@ -18,12 +18,16 @@ TRIBITS_SUBPACKAGE(context) ##---------------------------------------------------------------------------## # C/C++ headers SET(MOC_HEADERS + navigationmodel.hh + navigationwidget.hh + navigationactionmanager.hh qcpcontext.hh qcpcustom.hh qcpoptionsdialog.hh ) SET(HEADERS ${MOC_HEADERS} + navigationitem.hh qcpgraphdata.hh ) # @@ -39,6 +43,11 @@ QT5_ADD_RESOURCES(RESOURCE_RESULT ) # C/C++ source SET(SOURCES + ../qcpcore/value.cc # hack until TriBITS is removed + navigationitem.cc + navigationmodel.cc + navigationwidget.cc + navigationactionmanager.cc qcpcontext.cc qcpcustom.cc qcpoptionsdialog.cc diff --git a/qcpcontext/cmake/Dependencies.cmake b/qcpcontext/cmake/Dependencies.cmake index ef65c07f91b641c00981df882f162f9e68c47592..b0b7256ac0300b58f15f4a8d224009c4da1d5c6f 100644 --- a/qcpcontext/cmake/Dependencies.cmake +++ b/qcpcontext/cmake/Dependencies.cmake @@ -3,7 +3,7 @@ ## Jordan P. Lefebvre ##---------------------------------------------------------------------------## TRIBITS_PACKAGE_DEFINE_DEPENDENCIES( - LIB_REQUIRED_PACKAGES qcpQtPropertyBrowser qcpQCustomPlot radixcore + LIB_REQUIRED_PACKAGES qcpQtPropertyBrowser qcpQCustomPlot LIB_REQUIRED_TPLS Qt5PrintSupport Qt5Widgets Qt5Gui Qt5Core ) ##---------------------------------------------------------------------------## diff --git a/qcpcontext/navigationactionmanager.cc b/qcpcontext/navigationactionmanager.cc new file mode 100644 index 0000000000000000000000000000000000000000..db2ad7aca0da7ce6eab7eb05fea54fa7cec942bb --- /dev/null +++ b/qcpcontext/navigationactionmanager.cc @@ -0,0 +1,66 @@ +#include "qcpcontext/navigationactionmanager.hh" + +#include <QHash> +#include <QList> +#include <QMenu> + +#include "qcpcore/bug.hh" + +namespace qcp +{ +class NavigationActionManager::PImpl +{ + public: + QHash<NavigationItem *, QList<std::pair<QString, std::function<void()>>>> + item_functions; + ~PImpl(); +}; + +NavigationActionManager::NavigationActionManager(QObject *parent) + : QObject(parent) + , p(new PImpl) +{ +} + +NavigationActionManager::PImpl::~PImpl() {} + +NavigationActionManager::~NavigationActionManager() { delete p; } + +void NavigationActionManager::clearActions(NavigationItem *item) +{ + p->item_functions.remove(item); +} + +void NavigationActionManager::registerAction(NavigationItem *item, QString text, + std::function<void()> functor) +{ + auto item_it = p->item_functions.find(item); + if (item_it == p->item_functions.end()) + { + // create a new reference + QList<std::pair<QString, std::function<void()>>> list; + list.append(std::make_pair(text, functor)); + p->item_functions.insert(item, list); + } + else + { + item_it.value().append(std::make_pair(text, functor)); + } +} + +void NavigationActionManager::itemActions(NavigationItem *item, + QMenu *menu) const +{ + // + // Get the list of functors + auto functor_it = p->item_functions.find(item); + if (functor_it == p->item_functions.end()) return; + + auto &list = functor_it.value(); + for (int i = 0; i < list.length(); ++i) + { + menu->addAction(list[i].first, list[i].second); + } +} + +} // namespace qcp diff --git a/qcpcontext/navigationactionmanager.hh b/qcpcontext/navigationactionmanager.hh new file mode 100644 index 0000000000000000000000000000000000000000..ff1a31039604b0cd89c4cf0f4cb8c8018c350fba --- /dev/null +++ b/qcpcontext/navigationactionmanager.hh @@ -0,0 +1,33 @@ +#ifndef QCP_QCPCONTEXT_NAVIGATIONACTIONMANAGER_HH_ +#define QCP_QCPCONTEXT_NAVIGATIONACTIONMANAGER_HH_ +#include <QAction> +#include <QList> +#include <QObject> + +#include <functional> +#include <memory> + +#include "qcpcore/declspec.hh" +#include "qcpcontext/navigationitem.hh" + +namespace qcp +{ +class QCP_PUBLIC NavigationActionManager : public QObject +{ + Q_OBJECT + public: + NavigationActionManager(QObject *parent = nullptr); + ~NavigationActionManager(); + + void clearActions(NavigationItem *item); + void registerAction(NavigationItem *item, QString text, + std::function<void()> functor); + void itemActions(NavigationItem *item, QMenu *menu) const; + + private: + class PImpl; + PImpl *p; +}; +} // namespace qcp + +#endif /** QCP_QCPCONTEXT_NAVIGATIONACTIONMANAGER_HH_ */ diff --git a/qcpcontext/navigationitem.cc b/qcpcontext/navigationitem.cc new file mode 100644 index 0000000000000000000000000000000000000000..9394aab2e1729b7ce2f709d6802d4e8fa64832ae --- /dev/null +++ b/qcpcontext/navigationitem.cc @@ -0,0 +1,126 @@ +#include "qcpcontext/navigationitem.hh" +#include "qcpcore/bug.hh" + +#include <QList> + +namespace qcp +{ +class NavigationItem::PImpl +{ + public: + NavigationItem *parent = nullptr; + QList<NavigationItem *> children; + QList<QVariant> data; + QList<QString> tooltips; + bool checked = false; + int type = 0; + ~PImpl(); +}; + +NavigationItem::PImpl::~PImpl() { qDeleteAll(children); } + +NavigationItem::NavigationItem(QVariant data, NavigationItem *parentItem) + : p(new PImpl()) +{ + p->parent = parentItem; + p->data.append(data); +} + +NavigationItem::NavigationItem(QList<QVariant> data, NavigationItem *parentItem) + : p(new PImpl()) +{ + p->parent = parentItem; + p->data = data; +} + +NavigationItem::NavigationItem(NavigationItem *parentItem) + : p(new PImpl()) +{ + p->parent = parentItem; +} + +NavigationItem::~NavigationItem() { delete p; } + +int NavigationItem::type() const { return p->type; } + +void NavigationItem::setType(int type) { p->type = type; } + +void NavigationItem::addChild(NavigationItem *child) +{ + p->children.append(child); +} + +int NavigationItem::childCount() const { return p->children.size(); } + +int NavigationItem::columnCount() const +{ + // recursively find the largest number of columns from children + int count = p->data.size(); + for (int ci = 0; ci < p->children.size(); ++ci) + { + count = std::max(count, p->children[ci]->columnCount()); + } + return count; +} + +QVariant NavigationItem::data(int column, int role) const +{ + if (Qt::DisplayRole == role) + { + if (column >= p->data.size()) return QVariant(); + return p->data.value(column); + } + else if (Qt::ToolTipRole == role) + { + if (column < p->tooltips.size()) return p->tooltips.value(column); + } + else if (Qt::CheckStateRole == role) + { + return static_cast<int>(isChecked() ? Qt::Checked : Qt::Unchecked); + } + return QVariant(); +} + +void NavigationItem::setData(int column, QVariant value, int role) +{ + if (Qt::DisplayRole == role) + { + int size = p->data.size(); + for (int i = size; i <= column; ++i) + { + p->data.append(QVariant()); + } + p->data[column] = value; + } + else if (Qt::ToolTipRole == role) + { + p->tooltips.insert(column, value.toString()); + } +} + +int NavigationItem::row() const +{ + if (p->parent) + { + return p->parent->p->children.indexOf(const_cast<NavigationItem *>(this)); + } + return 0; +} + +NavigationItem *NavigationItem::parentItem() { return p->parent; } + +bool NavigationItem::isChecked() const { return p->checked; } + +void NavigationItem::setChecked(bool checked) { p->checked = checked; } + +NavigationItem *NavigationItem::child(int row) +{ + return p->children.value(row); +} + +bool NavigationItem::disown(NavigationItem *child) +{ + return p->children.removeOne(child); +} + +} // namespace qcp diff --git a/qcpcontext/navigationitem.hh b/qcpcontext/navigationitem.hh new file mode 100644 index 0000000000000000000000000000000000000000..1fce735afbc88618280650276417984a43e7e735 --- /dev/null +++ b/qcpcontext/navigationitem.hh @@ -0,0 +1,38 @@ +#ifndef QCP_QCPCONTEXT_NAVIGATIONITEM_HH_ +#define QCP_QCPCONTEXT_NAVIGATIONITEM_HH_ +#include <QVariant> + +#include <memory> + +#include "qcpcore/declspec.hh" +namespace qcp +{ +class QCP_PUBLIC NavigationItem +{ + public: + NavigationItem(QVariant data, NavigationItem *parentItem = nullptr); + NavigationItem(QList<QVariant> data, NavigationItem *parentItem = nullptr); + NavigationItem(NavigationItem *parentItem = nullptr); + ~NavigationItem(); + + int type() const; + void setType(int type); + void addChild(NavigationItem *child); + NavigationItem *child(int row); + bool disown(NavigationItem *child); + int childCount() const; + int columnCount() const; + QVariant data(int column, int role = Qt::DisplayRole) const; + void setData(int column, QVariant value, int role = Qt::DisplayRole); + int row() const; + NavigationItem *parentItem(); + bool isChecked() const; + void setChecked(bool checked); + + private: + class PImpl; + PImpl *p; +}; +} // namespace qcp + +#endif diff --git a/qcpcontext/navigationmodel.cc b/qcpcontext/navigationmodel.cc new file mode 100644 index 0000000000000000000000000000000000000000..533ff5a4afab9be26cdc5df9410d86021dabe39a --- /dev/null +++ b/qcpcontext/navigationmodel.cc @@ -0,0 +1,205 @@ +#include "qcpcontext/navigationmodel.hh" +#include "qcpcontext/navigationitem.hh" + +#include "qcpcore/bug.hh" + +#include <QList> + +namespace qcp +{ +class NavigationModel::PImpl +{ + public: + NavigationItem* root = nullptr; + + ~PImpl(); +}; + +NavigationModel::PImpl::~PImpl() +{ + if (root) delete root; +} +NavigationModel::NavigationModel(QObject* parent) + : QAbstractItemModel(parent) + , p(new PImpl()) +{ + p->root = new NavigationItem(); +} + +NavigationModel::~NavigationModel() { delete p; } + +QVariant NavigationModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) return QVariant(); + + NavigationItem* item = toItem(index); + return item->data(index.column(), role); +} + +bool NavigationModel::setData(const QModelIndex& index, const QVariant& value, + int role) +{ + NavigationItem* item = toItem(index); + if (Qt::CheckStateRole == role) + { + item->setChecked(value.toBool()); + // signal selection/deselection + if (item->isChecked()) + emit itemChecked(index); + else + emit itemUnChecked(index); + emit dataChanged(index, index, {Qt::CheckStateRole}); + return true; + } + else if (Qt::ToolTipRole == role) + { + item->setData(index.column(), value, role); + } + return false; +} + +Qt::ItemFlags NavigationModel::flags(const QModelIndex& index) const +{ + if (!index.isValid()) return QAbstractItemModel::flags(index); + + Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable; + if (index.column() == 0) flags |= Qt::ItemIsUserCheckable; + + return flags; +} + +QVariant NavigationModel::headerData(int section, Qt::Orientation orientation, + int role) const +{ + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) + return p->root->data(section); + + return QVariant(); +} + +QModelIndex NavigationModel::index(int row, int column, + const QModelIndex& parent) const +{ + if (parent.isValid() && parent.column() != 0) return QModelIndex(); + + NavigationItem* parentItem = toItem(parent); + + if (!parentItem) return QModelIndex(); + + NavigationItem* childItem = parentItem->child(row); + if (childItem) return createIndex(row, column, childItem); + return QModelIndex(); +} + +QModelIndex NavigationModel::parent(const QModelIndex& index) const +{ + if (!index.isValid()) return QModelIndex(); + + NavigationItem* childItem = toItem(index); + NavigationItem* parentItem = childItem ? childItem->parentItem() : nullptr; + + if (parentItem == p->root || !parentItem) return QModelIndex(); + + return createIndex(parentItem->row(), 0, parentItem); +} + +int NavigationModel::rowCount(const QModelIndex& parent) const +{ + NavigationItem* parentItem = toItem(parent); + + return parentItem ? parentItem->childCount() : 0; +} + +int NavigationModel::columnCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return static_cast<NavigationItem*>(parent.internalPointer()) + ->columnCount(); + else + return p->root->columnCount(); +} + +bool NavigationModel::removeChild(NavigationItem* parent, NavigationItem* item) +{ + if (parent == nullptr) return false; + for (int ri = 0; ri < parent->childCount(); ++ri) + { + NavigationItem* child = parent->child(ri); + + if (item == child) + { + QModelIndex qIndex = createIndex(ri, 0, child); + return removeRow(ri, qIndex); + } + else + { + qcp_tagged_line("Recursing down parent(" << parent << ") child(" + << child << ")"); + if (removeChild(child, item)) return true; + } + } + return false; +} + +bool NavigationModel::removeItem(NavigationItem* item) +{ + return removeChild(p->root, item); +} + +bool NavigationModel::removeRows(int row, int count, const QModelIndex& index) +{ + QVector<NavigationItem*> deleteList; + beginRemoveRows(index.parent(), row, row + count - 1); + + NavigationItem* parentItem = toItem(index.parent()); + for (int ci = 0; ci < count; ++ci) + { + NavigationItem* child = parentItem->child(row); + qcp_tagged_line("Remove child(" << child << ") from parent (" + << parentItem << ") at row(" << row + << ")"); + parentItem->disown(child); + deleteList.push_back(child); + } + + endRemoveRows(); + qDeleteAll(deleteList); + return true; +} + +NavigationItem* NavigationModel::rootItem() { return p->root; } + +NavigationItem* NavigationModel::addItem(QVariant value, NavigationItem* parent) +{ + beginResetModel(); + NavigationItem* newItem = new NavigationItem(value, parent); + parent->addChild(newItem); + endResetModel(); + return newItem; +} + +NavigationItem* NavigationModel::toItem(const QModelIndex& index) const +{ + if (index.isValid()) + { + NavigationItem* item = nullptr; + item = static_cast<NavigationItem*>(index.internalPointer()); + if (item) return item; + } + + return p->root; +} + +void NavigationModel::clear() +{ + int numKids = p->root->childCount(); + for (int ki = 0; ki < numKids; ++ki) + { + NavigationItem* kid = p->root->child(0); + if (p->root->disown(kid)) + { + delete kid; + } + } +} +} // namespace qcp diff --git a/qcpcontext/navigationmodel.hh b/qcpcontext/navigationmodel.hh new file mode 100644 index 0000000000000000000000000000000000000000..644eda71d7695ec87a2b8373355601125b6e532d --- /dev/null +++ b/qcpcontext/navigationmodel.hh @@ -0,0 +1,51 @@ +#ifndef QCP_QCPCONTEXT_NAVIGATIONMODEL_HH_ +#define QCP_QCPCONTEXT_NAVIGATIONMODEL_HH_ +#include <QAbstractItemModel> +#include <QObject> + +#include <memory> + +#include "qcpcore/declspec.hh" +namespace qcp +{ +// Forward declaration +class NavigationItem; + +class QCP_PUBLIC NavigationModel : public QAbstractItemModel +{ + Q_OBJECT + public: + NavigationModel(QObject *parent = nullptr); + ~NavigationModel(); + + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, + int role = Qt::EditRole) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + QModelIndex index(int row, int column, + const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + bool removeItem(NavigationItem *item); + bool removeRows(int row, int count, + const QModelIndex &index = QModelIndex()) override; + NavigationItem *rootItem(); + NavigationItem *addItem(QVariant value, NavigationItem *parent); + NavigationItem *toItem(const QModelIndex &index) const; + void clear(); + + signals: + void itemChecked(const QModelIndex &index); + void itemUnChecked(const QModelIndex &index); + + private: + bool removeChild(NavigationItem *parent, NavigationItem *item); + class PImpl; + PImpl *p; +}; +} // namespace qcp + +#endif diff --git a/qcpcontext/navigationwidget.cc b/qcpcontext/navigationwidget.cc new file mode 100644 index 0000000000000000000000000000000000000000..01cbe0c0996a071a011d22001f294dd27463486b --- /dev/null +++ b/qcpcontext/navigationwidget.cc @@ -0,0 +1,192 @@ +#include "qcpcontext/navigationwidget.hh" +#include "qcpcontext/navigationactionmanager.hh" +#include "qcpcontext/navigationitem.hh" +#include "qcpcontext/navigationmodel.hh" + +#include "qcpcore/bug.hh" + +#include <QClipboard> +#include <QEvent> +#include <QFileDialog> +#include <QFileInfo> +#include <QHeaderView> +#include <QKeyEvent> +#include <QLabel> +#include <QLineEdit> +#include <QMenu> +#include <QMessageBox> +#include <QStandardItemModel> +#include <QTextDocument> +#include <QToolTip> +#include <QTreeView> +#include <QtDebug> + +namespace qcp +{ +class QCP_PUBLIC NavigationItemSortFilterProxyModel::PImpl +{ + public: + QHash<QModelIndex, bool> seen; +}; + +NavigationItemSortFilterProxyModel::NavigationItemSortFilterProxyModel( + QObject* parent) + : QSortFilterProxyModel(parent) + , p(new PImpl()) +{ +} + +NavigationItemSortFilterProxyModel::~NavigationItemSortFilterProxyModel() +{ + delete p; +} + +bool NavigationItemSortFilterProxyModel::accepts(const QModelIndex& index) const +{ + const QString& text = index.data().toString(); + const QRegExp& regex = filterRegExp(); + + bool accept = text.contains(regex); + + for (int i = 0, ie = sourceModel()->rowCount(index); i < ie; i++) + { + if (accepts(sourceModel()->index(i, 0, index))) + { + accept = true; + break; + } + } + + p->seen[index] = accept; + return accept; +} + +void NavigationItemSortFilterProxyModel::clearCache() { p->seen.clear(); } + +bool NavigationItemSortFilterProxyModel::filterAcceptsRow( + int source_row, const QModelIndex& source_parent) const +{ + const QModelIndex& index = sourceModel()->index(source_row, 0, source_parent); + + if (p->seen.contains(index)) + { + return p->seen.value(index); + } + + return accepts(index); +} + +class QCP_PUBLIC NavigationWidget::PImpl +{ + public: + QLineEdit* filter; + NavigationModel* model; + NavigationActionManager* action_manager; + NavigationItemSortFilterProxyModel* proxy; + QTreeView* view; +}; + +NavigationWidget::NavigationWidget(QWidget* parent) + : QWidget(parent) + , p(new PImpl()) +{ + initMembers(); + initLayout(); +} + +NavigationWidget::~NavigationWidget() { delete p; } + +NavigationModel* NavigationWidget::navigationModel() { return p->model; } + +NavigationActionManager* NavigationWidget::navigationActionManager() +{ + return p->action_manager; +} + +void NavigationWidget::contextMenuRequested(QPoint point) +{ + qcp_tagged_line("contextMenuRequested()"); + QModelIndex pindex = p->view->indexAt(point); + QModelIndex index = p->proxy->mapToSource(pindex); + NavigationItem* item = static_cast<NavigationItem*>(index.internalPointer()); + + QMenu m; + p->action_manager->itemActions(item, &m); + m.exec(p->view->viewport()->mapToGlobal(point)); +} + +void NavigationWidget::initLayout() +{ + mVertLayout = new QVBoxLayout(this); + mVertLayout->addWidget(p->filter); + mVertLayout->addWidget(p->view); +} + +void NavigationWidget::initMembers() +{ + // allocate + p->filter = new QLineEdit(this); + p->model = new NavigationModel(this); + p->proxy = new NavigationItemSortFilterProxyModel(this); + p->view = new QTreeView(this); + p->action_manager = new NavigationActionManager(this); + // init properties + p->filter->setPlaceholderText("Filter"); + + p->view->installEventFilter(this); + p->view->setContextMenuPolicy(Qt::CustomContextMenu); + p->view->setEditTriggers(QAbstractItemView::NoEditTriggers); + p->view->setModel(p->proxy); + p->view->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + p->view->header()->setStretchLastSection(false); + p->view->header()->setVisible(false); + + p->proxy->setSourceModel(p->model); + + // shortcuts + mActivateEnter = new QShortcut(p->view); + mActivateReturn = new QShortcut(p->view); + mCollapseBranch = new QShortcut(p->view); + + mActivateEnter->setContext(Qt::WidgetShortcut); + mActivateEnter->setKey(Qt::Key_Enter); + + mActivateReturn->setContext(Qt::WidgetShortcut); + mActivateReturn->setKey(Qt::Key_Return); + + mCollapseBranch->setContext(Qt::WidgetShortcut); + mCollapseBranch->setKey(Qt::Key_Left); + + // + // connect signals/slots + connect(p->filter, SIGNAL(textChanged(QString)), this, + SLOT(filterModel(QString))); + connect(p->view, SIGNAL(customContextMenuRequested(QPoint)), this, + SLOT(contextMenuRequested(QPoint))); +} +void NavigationWidget::filterModel(const QString& pattern) +{ + // regex used to filter the model + QRegExp regex(pattern, Qt::CaseInsensitive, QRegExp::RegExp2); + + // update the model + p->proxy->clearCache(); + p->proxy->setFilterRegExp(regex); + + // update the tree + if (pattern.isEmpty()) + { + p->view->collapseAll(); + + for (int i = 0, ie = p->model->rowCount(); i < ie; i++) + { + p->view->expand(p->proxy->mapFromSource(p->model->index(i, 0))); + } + } + else + { + p->view->expandAll(); + } +} + +} // namespace qcp diff --git a/qcpcontext/navigationwidget.hh b/qcpcontext/navigationwidget.hh new file mode 100644 index 0000000000000000000000000000000000000000..9f6e2fb0a94b858d360bec684ad1d0ade768a83d --- /dev/null +++ b/qcpcontext/navigationwidget.hh @@ -0,0 +1,87 @@ +#ifndef QCP_QCPCONTEXT_NAVIGATIONWIDGET_HH_ +#define QCP_QCPCONTEXT_NAVIGATIONWIDGET_HH_ + +#include <QShortcut> +#include <QSortFilterProxyModel> +#include <QStandardItemModel> +#include <QStyledItemDelegate> +#include <QVBoxLayout> +#include <QWidget> + +#include "qcpcore/declspec.hh" + +#include <memory> + +QT_BEGIN_NAMESPACE +class QStandardItem; +class QTreeView; +QT_END_NAMESPACE + +namespace qcp +{ +// Forward declaration +class NavigationItem; +class NavigationModel; +class NavigationActionManager; + +class QCP_PUBLIC NavigationItemSortFilterProxyModel + : public QSortFilterProxyModel +{ + Q_OBJECT + + public: + NavigationItemSortFilterProxyModel(QObject* parent = nullptr); + ~NavigationItemSortFilterProxyModel(); + + public slots: + void clearCache(); + + protected: + bool filterAcceptsRow(int row, const QModelIndex& parent) const; + + private: + bool accepts(const QModelIndex& index) const; + class PImpl; + PImpl* p; +}; + +class QCP_PUBLIC NavigationWidget : public QWidget +{ + Q_OBJECT + + public: + NavigationWidget(QWidget* parent = nullptr); + ~NavigationWidget(); + + void collapse(QStandardItem* item); + + void expand(QStandardItem* item); + NavigationModel* navigationModel(); + NavigationActionManager* navigationActionManager(); + + QTreeView* navigationTree() const; + private slots: + void contextMenuRequested(QPoint); + public slots: + void filterModel(const QString& pattern); + + signals: + void itemDoubleClicked(const QModelIndex& index); + void itemClicked(const QModelIndex& index); + void itemChecked(const QModelIndex& index); + void itemUnchecked(const QModelIndex& index); + + private: + QVBoxLayout* mVertLayout; + QShortcut *mActivateEnter, *mActivateReturn, *mCollapseBranch; + + void initLayout(); + + void initMembers(); + + class PImpl; + PImpl* p; +}; +} // namespace qcp + +#endif /** QCP_QCPCONTEXT_NAVIGATIONWIDGET_HH_ */ diff --git a/qcpcontext/qcpcontext.cc b/qcpcontext/qcpcontext.cc index 2ac1b9c46a5d1d76f5e30aca000cffe88875c7b5..7795a081b319d8211fe4d2a5311354c304d49050 100644 --- a/qcpcontext/qcpcontext.cc +++ b/qcpcontext/qcpcontext.cc @@ -60,8 +60,8 @@ QCPContext::~QCPContext() if (mOptionsDialog) delete mOptionsDialog; } // QCPContext::~Plot -CustomPlot *QCPContext::qcp() { return mQCPPlot; } // QCPContext::qcp -const CustomPlot *QCPContext::qcp() const +CustomPlot *QCPContext::qcplot() { return mQCPPlot; } // QCPContext::qcp +const CustomPlot *QCPContext::qcplot() const { return mQCPPlot; } // QCPContext::qcp @@ -604,9 +604,9 @@ void QCPContext::legendDoubleClick(QCPLegend *legend, void QCPContext::mouseMove(QMouseEvent *event) { - auto x = qcp()->xAxis->pixelToCoord(event->x()); - auto y = qcp()->yAxis->pixelToCoord(event->y()); - auto yl = qcp()->yAxis->pixelToCoord(event->y() + 16); + auto x = qcplot()->xAxis->pixelToCoord(event->x()); + auto y = qcplot()->yAxis->pixelToCoord(event->y()); + auto yl = qcplot()->yAxis->pixelToCoord(event->y() + 16); // mouse move is outside axis range // we don't show the label @@ -666,7 +666,7 @@ void QCPContext::mouseMove(QMouseEvent *event) vAlign = Qt::AlignBottom; } - qcp()->axisRect()->insetLayout()->setInsetAlignment(0, vAlign | hAlign); + qcplot()->axisRect()->insetLayout()->setInsetAlignment(0, vAlign | hAlign); } // need to redraw @@ -681,7 +681,7 @@ void QCPContext::mousePress(QMouseEvent *event) setSelectionRectMode(QCP::srmNone); } - mLegendDrag = qcp()->legend->selectTest(event->pos(), false) >= 0; + mLegendDrag = qcplot()->legend->selectTest(event->pos(), false) >= 0; } void QCPContext::mouseRelease(QMouseEvent *event) @@ -694,9 +694,9 @@ void QCPContext::mouseRelease(QMouseEvent *event) void QCPContext::plottableClicked(QCPAbstractPlottable *plottable, QMouseEvent *event) { - auto x = qcp()->xAxis->pixelToCoord(event->x()); - auto y = qcp()->yAxis->pixelToCoord(event->y()); - auto yl = qcp()->yAxis->pixelToCoord(event->y() + 16); // label adjustment + auto x = qcplot()->xAxis->pixelToCoord(event->x()); + auto y = qcplot()->yAxis->pixelToCoord(event->y()); + auto yl = qcplot()->yAxis->pixelToCoord(event->y() + 16); // label adjustment mLabel->position->setCoords(x, yl); mLabel->setPositionAlignment(Qt::AlignTop | Qt::AlignLeft); @@ -711,7 +711,7 @@ void QCPContext::plottableClicked(QCPAbstractPlottable *plottable, void QCPContext::selectionChanged() { // plot object - auto plot = qcp(); + auto plot = qcplot(); // synchronize selection of graphs with selection of corresponding legend // items multiple selection can occur diff --git a/qcpcontext/qcpcontext.hh b/qcpcontext/qcpcontext.hh index 1b84c8d8452920b0e3aa1130a12b63a63c615b24..ca2834e73a853562a0611ab9e14630c498aaba5a 100644 --- a/qcpcontext/qcpcontext.hh +++ b/qcpcontext/qcpcontext.hh @@ -66,8 +66,8 @@ class QCPContext : public QWidget * @brief qcp Retrieve the raw QCustomPlot * @return QCustomPlot* */ - CustomPlot *qcp(); - const CustomPlot *qcp() const; + CustomPlot *qcplot(); + const CustomPlot *qcplot() const; /** * @brief createQCPGraph creates a slot in this Plot for a QCPGraph diff --git a/qcpcontext/qcpgraphdata.hh b/qcpcontext/qcpgraphdata.hh index 1a53de4e6248db238b2d2efcc845058d225d2c2b..443cfaa64fc66fb0948a648eb352bc0ccc5b2976 100644 --- a/qcpcontext/qcpgraphdata.hh +++ b/qcpcontext/qcpgraphdata.hh @@ -7,7 +7,7 @@ #ifndef QCPGRAPHDATA_HH_ #define QCPGRAPHDATA_HH_ -#include "radixcore/visibility.hh" +#include "qcpcore/declspec.hh" #include <memory> @@ -16,7 +16,7 @@ namespace qcp { -class RADIX_PUBLIC QCPGraphData +class QCP_PUBLIC QCPGraphData { public: typedef std::shared_ptr<QCPGraphData> SP; diff --git a/qcpcontext/qcpoptionsdialog.cc b/qcpcontext/qcpoptionsdialog.cc index c1be8d8233a48a1d68e5f42b8de60f7c36297e3e..56ddb27b1acb0a928a6cb7f7c17a4f0df051e663 100644 --- a/qcpcontext/qcpoptionsdialog.cc +++ b/qcpcontext/qcpoptionsdialog.cc @@ -92,7 +92,7 @@ void QCPOptionsDialog::initAxisProperties() { void QCPOptionsDialog::initBarsProperties() { // plot - auto qcp = mPlot->qcp(); + auto qcp = mPlot->qcplot(); // list of color maps QList<QCPBars *> bs; @@ -136,7 +136,7 @@ void QCPOptionsDialog::initBarsProperties() { void QCPOptionsDialog::initColorMapProperties() { // plot - auto qcp = mPlot->qcp(); + auto qcp = mPlot->qcplot(); // list of color maps QList<QCPColorMap *> cms; @@ -179,7 +179,7 @@ void QCPOptionsDialog::initColorMapProperties() { void QCPOptionsDialog::initGraphProperties() { // plot - auto qcp = mPlot->qcp(); + auto qcp = mPlot->qcplot(); // no graphs if (qcp->graphCount() == 0) { @@ -215,7 +215,7 @@ void QCPOptionsDialog::initGraphProperties() { void QCPOptionsDialog::initCurveProperties() { // plot - auto qcp = mPlot->qcp(); + auto qcp = mPlot->qcplot(); // list of curves QList<QCPCurve *> cms; diff --git a/qcpcore/bug.hh b/qcpcore/bug.hh new file mode 100644 index 0000000000000000000000000000000000000000..271d1c84c058a13fe7f39e9aeb438044e6ad509e --- /dev/null +++ b/qcpcore/bug.hh @@ -0,0 +1,258 @@ +#ifndef QCP_QCPCORE_BUG_HH_ +#define QCP_QCPCORE_BUG_HH_ + +#ifndef DEBUG_OUTPUT +#define DEBUG_OUTPUT 0 +#define DEBUG_CALL(c) +#else +#define DEBUG_CALL(c) c +#endif + +// default to c++ compiler +/* + * @file: bug.hh + * @author: Jordan P. Lefebvre, lefebvrejp@ornl.gov + * @brief Debug functionality for including debug output \ + * in your source without a performance penalty. + * This is an include ONLY package. There is no library to link against + * Each function is preprocessed to be included or not. + * If DEBUG_OUTPUT is defined the MACROs are included. + * + * Available MACROs: + * qcp(arg) - std::cerr << arg - Pushes content to stderr + * qcp_line(arg) - std::cerr << arg << std::endl - Push newline terminated + * content to stderr + * qcp_eol() - std::cerr << std::endl - Push newline to stderr + * qcp(arg) - std::cerr << arg - Pushes content to stderr + * qcp_warning(arg) - std::cerr << arg << std::endl - Push newline terminated + * content to stderr + * qcp_tagged_line(arg) - Same as qcp_line, prefixed with + * FILE and LINE + * qcp_tagged(arg) - Same as qcp, prefixed with FILE and LINE + * qcp_tagged_warning(arg) - Same as qcp_warning, prefixed with FILE and Line + * qcp_block(block) - block - Simply places code block in preprocessor + */ +#include <cstdio> +#include <iostream> +#if DEBUG_OUTPUT & 1 +#ifndef qcp_stream +#define qcp_stream std::cerr +#endif +#define qcp_define_stream +#define qcp_line(arg) qcp_stream << arg << std::endl +#define qcp_eol() qcp_stream << std::endl +#define qcp_flush_line(arg) \ + qcp_stream << arg << std::endl; \ + fflush(stderr) +#define qcp(arg) qcp_stream << arg +#define qcp_warning(arg) qcp_stream << arg << std::endl +#define qcp_tagged_warning(arg) \ + qcp_stream << __FILE__ << ":" << __LINE__ << ": " << arg << std::endl +#define qcp_flush_warning(arg) \ + qcp_stream << arg << std::endl; \ + fflush(stderr) +#define qcp_tagged_line(arg) \ + qcp_stream << __FILE__ << ":" << __LINE__ << ": " << arg << std::endl +#define qcp_flush_tagged_line(arg) \ + qcp_stream << __FILE__ << ":" << __LINE__ << ": " << arg << std::endl; \ + fflush(stderr) +#define qcp_tagged(arg) \ + qcp_stream << __FILE__ << ":" << __LINE__ << ": " << arg +#define qcp_flush_tagged_warning(arg) \ + qcp_stream << __FILE__ << ":" << __LINE__ << ": " << arg << std::endl; \ + fflush(stderr) +#define qcp_tagged_block(block) \ + qcp_stream << __FILE__ << ":" << __LINE__ << ":" << std::endl; \ + block +#define qcp_block(block) block +#else +#define qcp(arg) +#define qcp_line(arg) +#define qcp_eol() +#define qcp_flush_line(arg) +#define qcp_warning(arg) +#define qcp_flush_warning(arg) +#define qcp_tagged_line(arg) +#define qcp_flush_tagged_line(arg) +#define qcp_tagged(arg) +#define qcp_tagged_warning(arg) +#define qcp_flush_tagged_warning(arg) +#define qcp_tagged_block(block) +#define qcp_block(block) +#endif /* DEBUG_OUTPUT */ + +#include <sstream> +#include <stdexcept> +#ifndef QCP_DBC +#define QCP_DBC 0 +#endif + +// +// Defined all levels of QCP_DBC +// QCP_DBC = 1 enables Require +// QCP_DBC = 2 enables Check +// QCP_DBC = 4 enables Remember & Ensure +// QCP_DBC = 7 enables all +// Insist is always enabled +#if QCP_DBC & 1 +#define qcp_require(c) \ + if (!(c)) \ + { \ + std::ostringstream stream; \ + stream << __FILE__ << ":" << __LINE__ << " qcp_require(" << #c \ + << ") failed." << std::endl; \ + throw std::runtime_error(stream.str()); \ + } +#else +#define qcp_require(c) +#endif +#if QCP_DBC & 2 +#define qcp_check(c) \ + if (!(c)) \ + { \ + std::ostringstream stream; \ + stream << __FILE__ << ":" << __LINE__ << " qcp_check(" << #c \ + << ") failed." << std::endl; \ + throw std::runtime_error(stream.str()); \ + } +#else +#define qcp_check(c) +#endif +#if QCP_DBC & 4 +#define qcp_ensure(c) \ + if (!(c)) \ + { \ + std::ostringstream stream; \ + stream << __FILE__ << ":" << __LINE__ << " qcp_ensure(" << #c \ + << ") failed." << std::endl; \ + throw std::runtime_error(stream.str()); \ + } +#define qcp_remember(c) c +#else +#define qcp_ensure(c) +#define qcp_remember(c) +#endif + +#define qcp_insist(c, msg) \ + if (!(c)) \ + { \ + std::ostringstream stream; \ + stream << __FILE__ << ":" << __LINE__ << "qcp_insist(" << #c \ + << ") failed with this message:" << std::endl \ + << msg << std::endl; \ + throw std::runtime_error(stream.str()); \ + } + +#define qcp_not_implemented(msg) \ + { \ + std::ostringstream stream; \ + stream << __FILE__ << ":" << __LINE__ << " : " << msg \ + << " is not implemented. " << std::endl; \ + throw std::runtime_error(stream.str()); \ + } + +/// set default timing to off +#ifndef QCP_TIMING +#define QCP_TIMING 0 +#endif + +#include <chrono> +#include <ctime> +namespace qcp +{ +class Timer +{ + private: + bool mRunning; + std::chrono::steady_clock::time_point mStart; + std::chrono::steady_clock::time_point mEnd; + std::chrono::nanoseconds mDuration; + size_t mIntervals; + + public: + Timer() + : mRunning(false) + , mDuration(0) + , mIntervals(0) + { + } + void start() + { + qcp_check(!mRunning); + mRunning = true; + mIntervals++; + mStart = std::chrono::steady_clock::now(); + } + void stop() + { + qcp_check(mRunning); + mEnd = std::chrono::steady_clock::now(); + mRunning = false; + mDuration += + std::chrono::duration_cast<std::chrono::nanoseconds>(mEnd - mStart); + } + + std::chrono::nanoseconds::rep duration() const + { + qcp_check(!mRunning); + return mDuration.count(); + } + size_t intervals() const { return mIntervals; } + + double wall_clock() + { + qcp_require(!mRunning); + // 1e9 nanoseconds in a second + using seconds = std::chrono::duration<double, std::ratio<1000, 1>>; + return seconds(mEnd - mStart).count(); + } + + double sum_wall_clock() + { + // 1e9 nanoseconds in a second + using seconds = std::chrono::duration<double, std::ratio<1000, 1>>; + return seconds(mDuration).count(); + } + + bool running() const { return mRunning; }; +}; + +// class Timer +} // namespace qcp + +#if QCP_TIMING > 0 +#define qcp_timer(name) qcp::Timer name +#define qcp_timer_start(name) name.start() +#define qcp_timer_stop(name) name.stop() +#define qcp_timer_block(content) content +#else +#define qcp_timer(name) +#define qcp_timer_start(name) +#define qcp_timer_stop(name) +#define qcp_timer_block(content) +#endif +#if QCP_TIMING > 1 +#define qcp_timer_2(name) qcp::Timer name +#define qcp_timer_start_2(name) name.start() +#define qcp_timer_stop_2(name) name.stop() +#define qcp_timer_block_2(content) content +#else +#define qcp_timer_2(name) +#define qcp_timer_start_2(name) +#define qcp_timer_stop_2(name) +#define qcp_timer_block_2(content) +#endif + +#if QCP_TIMING > 2 +#define qcp_timer_3(name) qcp::Timer name +#define qcp_timer_start_3(name) name.start() +#define qcp_timer_stop_3(name) name.stop() +#define qcp_timer_block_3(content) content +#else +#define qcp_timer_3(name) +#define qcp_timer_start_3(name) +#define qcp_timer_stop_3(name) +#define qcp_timer_block_3(content) +#endif + +#endif /* QCP_QCPCORE_BUG_HH_*/ diff --git a/qcpcore/declspec.hh b/qcpcore/declspec.hh new file mode 100644 index 0000000000000000000000000000000000000000..d8d09f779c572be7153d345f283545554ac8e18e --- /dev/null +++ b/qcpcore/declspec.hh @@ -0,0 +1,10 @@ +#ifndef QCP_QCPCORE_VISIBILITY_HH_ +#define QCP_QCPCORE_VISIBILITY_HH_ +#if defined _WIN32 || defined __CYGWIN__ + #define QCP_PUBLIC __declspec(dllexport) + #define QCP_LOCAL +#else + #define QCP_PUBLIC + #define QCP_LOCAL __declspec(dllimport) +#endif +#endif /* QCP_QCPCORE_VISIBILITY_HH_*/ diff --git a/qcpcore/json.hh b/qcpcore/json.hh new file mode 100644 index 0000000000000000000000000000000000000000..2bc66ea1abf88ff671fa59555b52ba4b9f9fe252 --- /dev/null +++ b/qcpcore/json.hh @@ -0,0 +1,625 @@ +#ifndef QCP_QCPCORE_JSON_HH_ +#define QCP_QCPCORE_JSON_HH_ + +#include <cstring> +#include <fstream> +#include <iostream> +#include <map> +#include <sstream> +#include <vector> + +#include "qcpcore/value.hh" +#include "qcpcore/declspec.hh" + +//----------------------------------------------------------------------------- +// see www.json.org for parsing grammar +namespace qcp +{ +template <class value_type, class array_type, class object_type> +class QCP_PUBLIC JSONParserImpl +{ + public: + //------------------------------------------------------------------------- + JSONParserImpl() + { + literals[0] = "false"; + literals[1] = "null"; + literals[2] = "true"; + for (size_t i = 0; i < N_LITERALS; i++) + literal_lens[i] = strlen(literals[i]); + } + + //------------------------------------------------------------------------- + value_type& root() { return m_root; } + + //------------------------------------------------------------------------- + std::string last_error() + { + return m_last_error + " at line " + std::to_string(m_line) + " column " + + std::to_string(m_col); + } + + //------------------------------------------------------------------------- + // there can be only one value at root node + bool parse() + { + m_po = 0; + m_line = 1; + m_col = 1; + m_last_error = ""; + + m_root = value_type(); + bool result = parse_value(m_root); + if (!result) return false; + skip_whitespace(); + if (m_po != m_text.size()) + { + set_error("unexpected trailing character(s)"); + // clear text copy of json file + m_text.clear(); + return false; + } + // clear text copy of json file + m_text.clear(); + return true; + } + + //------------------------------------------------------------------------- + bool parse_from_stream(std::istream& in_stream) + { + in_stream.seekg(0, std::ios::end); + std::streampos file_size = in_stream.tellg(); + in_stream.seekg(0); + m_text.clear(); + m_text.resize(static_cast<unsigned>(file_size), '\0'); + in_stream.read(&m_text[0], file_size); + if (!in_stream) + { + set_error("could only read " + std::to_string(file_size) + " of " + + std::to_string(in_stream.gcount()) + " bytes"); + // clear text copy of json file + m_text.clear(); + return false; + } + return parse(); + } + + //------------------------------------------------------------------------- + bool parse_from_file(const std::string& fn) + { + std::ifstream file; + file.open(fn, std::ios::in | std::ios::binary | std::ios::ate); + if (!file.is_open()) + { + set_error("could not open file: " + fn); + return false; + } + bool result = parse_from_stream(file); + file.close(); + return result; + } + + private: + void set_error(const std::string& error) + { + // save the error meesage + m_last_error = error; + } + //------------------------------------------------------------------------- + bool is_whitespace() + { + char ch = m_text[m_po]; + return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'); + } + + //------------------------------------------------------------------------- + bool is_structural_character() + { + char ch = m_text[m_po]; + return (ch == ',' || ch == ':' || ch == '[' || ch == ']' || ch == '{' || + ch == '}'); + } + + //------------------------------------------------------------------------- + void skip_whitespace() + { + for (; m_po < m_text.size(); m_po++) + { + if (m_text[m_po] == ' ' || m_text[m_po] == '\t') + { + m_col++; + continue; + } + if (m_text[m_po] == '\r') + { + m_line++; + m_col = 1; + continue; + } + if (m_text[m_po] == '\n') + { + // treat \r\n as one new-line + if (m_po > 0 && m_text[m_po - 1] == '\r') continue; + m_line++; + m_col = 1; + continue; + } + break; + } + } + + //------------------------------------------------------------------------- + bool parse_array(value_type& value) + { + value = value_type(); + if (m_po >= m_text.size()) return false; + if (m_text[m_po] != '[') return false; + m_po++; + m_col++; + + value_type parent = array_type(); + bool found_child = false; + for (; m_po < m_text.size(); m_po++, m_col++) + { + skip_whitespace(); + if (m_text[m_po] == ']') break; + value_type child; + if (!parse_value(child)) + { + found_child = false; + if (m_po >= m_text.size()) + { + set_error("no closing bracket ']' for array"); + } + else if (m_text[m_po] == ']') + break; + } + else + { + found_child = true; + parent.as_array().push_back(child); + } + skip_whitespace(); + char ch = m_text[m_po]; + if (ch == ',') + { + if (!found_child) + { + set_error("missing value in array"); + return false; + } + continue; + } + else if (ch == ']') + break; + else + { + std::string err_msg = "invalid character '"; + err_msg += ch; + err_msg += "' in array"; + set_error(err_msg); + return false; + } + } + if (m_po >= m_text.size() || m_text[m_po] != ']' || m_last_error != "") + { + set_error("no closing bracket ']' for array"); + return false; + } + m_po++; + m_col++; + + value = parent; + return true; + } + + //------------------------------------------------------------------------- + // parsed to match the following regular expression: + // (-)? + // (0|([1-9][0-9]*)) + // (\.[0-9]+)? + // ([Ee][+-]?[0-9]+)? + bool parse_number(value_type& value) + { + bool is_float = false; + value = value_type(); + if (m_po >= m_text.size()) return false; + + size_t len = 0; + + // (-)? + if (m_text[m_po] == '-') + { + m_po++; + m_col++; + len++; + if (m_po >= m_text.size()) + { + set_error("invalid number (no digits after -)"); + return false; + } + } + + // (0|([1-9][0-9]*)) + char ch = m_text[m_po]; + if (m_po >= m_text.size() || !(ch >= '0' && m_text[m_po] <= '9')) + { + set_error("invalid number (no digits)"); + return false; + } + m_po++; + m_col++; + len++; + // [1-9][0-9]* + if (ch >= '1' && ch <= '9') + { + // [0-9]* + for (; m_po < m_text.size(); m_po++, m_col++, len++) + { + ch = m_text[m_po]; + if (!(ch >= '0' && ch <= '9')) break; + } + } + if (m_po >= m_text.size()) + { + try + { + value = value_type(std::stoi(std::string(&m_text[m_po - len], len))); + return true; + } + catch (...) + { + return false; + } + } + ch = m_text[m_po]; + + // (\.[0-9]+)? + if (ch == '.') + { + is_float = true; + m_po++; + m_col++; + len++; + if (m_po >= m_text.size()) + { + set_error("invalid number (no digits after decimal)"); + return false; + } + ch = m_text[m_po]; + size_t n_digits = 0; + // [0-9]+ + for (; m_po < m_text.size(); m_po++, m_col++, len++, n_digits++) + { + ch = m_text[m_po]; + if (!(ch >= '0' && ch <= '9')) break; + } + if (n_digits == 0) + { + set_error("invalid number (no digits after decimal)"); + return false; + } + } + + // ([Ee][+-]?[0-9]+)? + if (ch == 'E' || ch == 'e') + { + is_float = true; + m_po++; + m_col++; + len++; + if (m_po >= m_text.size()) + { + set_error("invalid number (no digits for exponent)"); + return false; + } + ch = m_text[m_po]; + // [+-]? + if (ch == '+' || ch == '-') + { + m_po++; + m_col++; + len++; + } + if (m_po >= m_text.size()) + { + set_error("invalid number (no digits for exponent)"); + return false; + } + + size_t n_digits = 0; + // [0-9]+ + for (; m_po < m_text.size(); m_po++, m_col++, len++, n_digits++) + { + ch = m_text[m_po]; + if (!(ch >= '0' && ch <= '9')) break; + } + if (n_digits == 0) + { + set_error("invalid number (no digits for exponent)"); + return false; + } + } + + try + { + double v = std::stod(std::string(&m_text[m_po - len], len)); + if (is_float) + value = value_type(v); + else + value = value_type(int(v >= 0 ? v + .5 : v - .5)); + + return true; + } + catch (...) + { + return false; + } + } + + //------------------------------------------------------------------------- + // str_known must be a null-terminated string + inline bool sub_str_eq(char* sub_str_unknown, const char* str_known) + { + size_t i = 0; + while (str_known[i] != '\0') + { + if (sub_str_unknown[i] != str_known[i]) return false; + i++; + } + return true; + } + + //------------------------------------------------------------------------- + // only 3 valid literals all in lower case: false, null, true + // TODO: need to refactor code to support null return (currently it is + // treated as an error) + bool parse_literal(value_type& value) + { + size_t len = 0; + value = value_type(); + for (; m_po + len < m_text.size(); len++) + { + char ch = m_text[m_po + len]; + if (!((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'))) break; + } + for (size_t i = 0; i < N_LITERALS; i++) + { + if (literal_lens[i] == len && sub_str_eq(&m_text[m_po], literals[i])) + { + m_po += len; + m_col += len; + if (std::string("true") == literals[i]) + { + value = value_type(true); + return true; + } + else if (std::string("false") == literals[i]) + { + value = value_type(false); + return true; + } + // "null" default is already handled + return true; + } + } + set_error("invalid literal"); + return false; + } + + //------------------------------------------------------------------------- + bool parse_object(value_type& value) + { + value = value_type(); + if (m_po >= m_text.size()) return false; + if (m_text[m_po] != '{') return false; + m_po++; + m_col++; + + value_type parent = object_type(); + for (; m_po < m_text.size(); m_po++, m_col++) + { + skip_whitespace(); + + if (m_po >= m_text.size() || m_text[m_po] == '}') break; + + // parse key + std::string key = parse_string_contents(); + if (m_last_error != "") return false; + skip_whitespace(); + + // parse ':' + if (m_po >= m_text.size() || m_text[m_po] != ':') + { + set_error("no ':' following key in object"); + return false; + } + m_po++; + m_col++; + + // parse value + bool result = false; + value_type child = value_type(); + result = parse_value(child); + if (!result) + { + set_error("missing value in object"); + return false; + } + else + { + parent.as_object()[key] = child; + } + skip_whitespace(); + + char ch = m_text[m_po]; + if (ch == ',') + continue; + else if (ch == '}') + break; + else + { + set_error("invalid character in object"); + return false; + } + } + if (m_po >= m_text.size() || m_text[m_po] != '}') + { + set_error("no closing curly bracket '}' for object"); + return false; + } + m_po++; + m_col++; + + value = parent; + return true; + } + + //------------------------------------------------------------------------- + inline bool parse_escape_seq(size_t* len) + { + if (m_po >= m_text.size()) return false; + if (m_text[m_po] != '\\') return false; + m_po++; + m_col++; + (*len)++; + if (m_po >= m_text.size()) + { + set_error("incomplete unicode character escape sequence in string"); + return false; + } + char ch = m_text[m_po]; + if (ch == '"' || ch == '\\' || ch == '/' || ch == 'b' || ch == 'f' || + ch == 'n' || ch == 'r' || ch == 't') + { + m_po++; + m_col++; + (*len)++; + return true; + } + else if (ch == 'u') + { + m_po++; + m_col++; + (*len)++; + size_t code_len = 0; + // parse 4-digit unicode character escape sequence + for (; m_po < m_text.size() && code_len < 4; + m_po++, m_col++, (*len)++, code_len++) + { + char ch = m_text[m_po]; + if (!((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F') || + (ch >= 'a' && ch <= 'f'))) + { + set_error("invalid unicode character escape sequence in string"); + return false; + } + } + if (code_len < 4) + { + set_error("incomplete unicode character escape sequence in string"); + return false; + } + return true; + } + set_error("invalid escape sequence in string"); + return false; + } + + //------------------------------------------------------------------------- + // parse a quoted string from m_text + // used for string values and object keys + // processes and discards leading and trailing quotes + // on success, returns string contents without quotes + // on error, returns empty string and sets m_last_error + std::string parse_string_contents() + { + if (m_po >= m_text.size() || m_text[m_po] != '"') + { + set_error("string missing opening quote"); + return ""; + } + m_po++; + m_col++; + for (size_t len = 0; m_po < m_text.size();) + { + char ch = m_text[m_po]; + // disallow control characters <= 0x1f + if (static_cast<unsigned>(ch) <= 0x1f) + { + set_error("invalid character in string"); + return ""; + } + if (ch == '\\') + { + bool ok = parse_escape_seq(&len); + if (!ok) return ""; + continue; + } + if (ch == '"') + { + m_po++; + m_col++; + return std::string(&m_text[m_po - 1 - len], len); + } + m_po++; + m_col++; + len++; + } + set_error("string missing closing quote"); + return ""; + } + + //------------------------------------------------------------------------- + bool parse_string(value_type& value) + { + value = value_type(); + std::string str = parse_string_contents(); + if (!m_last_error.empty()) return false; + value = value_type(str); + return true; + } + + //------------------------------------------------------------------------- + bool parse_value(value_type& value) + { + skip_whitespace(); + char ch = m_text[m_po]; + value = value_type(); + bool result = false; + if (ch == '"') + result = parse_string(value); + else if (ch == '[') + result = parse_array(value); + else if (ch == '{') + result = parse_object(value); + else if (ch == '-' || (ch >= '0' && ch <= '9')) + result = parse_number(value); + else if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) + result = parse_literal(value); + // TODO: need to handle ']', '}' and ',' differently from actual + // invalid characters + //~ else {} + return result; + } + + static const size_t N_LITERALS = 3; + const char* literals[N_LITERALS]; + size_t literal_lens[N_LITERALS]; + + value_type m_root; + + std::string m_text = ""; + + // current parsing offset, line and column + size_t m_po = 0; + size_t m_line = 1; + size_t m_col = 1; + + std::string m_last_error = ""; +}; + +typedef JSONParserImpl<Value, DataArray, DataObject> JSONParser; + +} // namespace qcp +#endif /** QCP_QCPCORE_JSON_HH_ */ diff --git a/qcpcore/value.cc b/qcpcore/value.cc new file mode 100644 index 0000000000000000000000000000000000000000..d2d63d07e6bb21ae6a48e7617ec1d86190bc1af5 --- /dev/null +++ b/qcpcore/value.cc @@ -0,0 +1,832 @@ +#include "qcpcore/value.hh" + +#include <cassert> +#include <cstdlib> // atoi/atof, etc +#include <cstring> // strdup, free +#include <sstream> +#include "qcpcore/bug.hh" + +namespace qcp +{ +Value::Value() + : m_allocated(false) + , m_type(TYPE_NULL) +{ +} +Value::Value(const Value& orig) { this->copy_from(orig); } +Value::Value(Value&& orig) + : m_allocated(orig.m_allocated) + , m_type(orig.m_type) + , m_data(orig.m_data) +{ + orig.m_allocated = false; + orig.m_type = TYPE_NULL; +} +Value::Value(bool v) +{ + m_data.m_bool = v; + m_type = TYPE_BOOLEAN; +} +Value::Value(int v) +{ + m_data.m_int = v; + m_type = TYPE_INTEGER; +} +Value::Value(double v) +{ + m_data.m_double = v; + m_type = TYPE_DOUBLE; +} +Value::Value(const char* v) +{ + qcp_require(v); + m_data.m_string = strdup(v); + m_type = TYPE_STRING; + m_allocated = true; +} +Value::Value(const std::string& v) +{ + m_data.m_string = strdup(v.c_str()); + m_type = TYPE_STRING; + m_allocated = true; +} +Value::Value(const DataArray& v) +{ + m_data.m_array = new DataArray(v); + m_type = TYPE_ARRAY; + m_allocated = true; +} +Value::Value(const DataObject& v) +{ + m_data.m_object = new DataObject(v); + m_type = TYPE_OBJECT; + m_allocated = true; +} +void Value::copy_from(const Value& orig) +{ + m_allocated = orig.m_allocated; + m_type = orig.m_type; + switch (m_type) + { + case TYPE_NULL: + break; + case TYPE_BOOLEAN: + m_data.m_bool = orig.m_data.m_bool; + break; + case TYPE_INTEGER: + m_data.m_int = orig.m_data.m_int; + break; + case TYPE_DOUBLE: + m_data.m_double = orig.m_data.m_double; + break; + case TYPE_STRING: + qcp_check(m_allocated); + m_data.m_string = strdup(orig.m_data.m_string); + break; + case TYPE_ARRAY: + qcp_check(m_allocated); + m_data.m_array = new DataArray(*orig.m_data.m_array); + qcp_ensure(m_data.m_array); + break; + case TYPE_OBJECT: + qcp_check(m_allocated); + m_data.m_object = new DataObject(*orig.m_data.m_object); + qcp_ensure(m_data.m_object); + break; + } +} + +Value::Type Value::type() const { return m_type; } + +std::string Value::categoryString() const +{ + switch (m_type) + { + case Type::TYPE_INTEGER: + case Type::TYPE_DOUBLE: + return "number"; + case Type::TYPE_STRING: + return "string"; + case Type::TYPE_OBJECT: + return "object"; + case Type::TYPE_ARRAY: + return "array"; + case Type::TYPE_BOOLEAN: + return "boolean"; + default: + return "null"; + } +} + +Value& Value::operator=(const Value& orig) +{ + // release any allocated memory + nullify(); + // copy from the originator + copy_from(orig); + return *this; +} +Value& Value::operator=(Value&& orig) +{ + // release any allocated memory + nullify(); + m_allocated = orig.m_allocated; + m_data = orig.m_data; + m_type = orig.m_type; + + orig.m_allocated = false; + orig.m_type = TYPE_NULL; + + return *this; +} +Value& Value::operator=(bool v) +{ + nullify(); + m_type = TYPE_BOOLEAN; + m_data.m_bool = v; + return *this; +} +Value& Value::operator=(int v) +{ + nullify(); + m_type = TYPE_INTEGER; + m_data.m_int = v; + return *this; +} +Value& Value::operator=(double v) +{ + nullify(); + m_type = TYPE_DOUBLE; + m_data.m_double = v; + return *this; +} +Value& Value::operator=(const char* v) +{ + nullify(); + qcp_require(v); + m_data.m_string = strdup(v); + m_type = TYPE_STRING; + m_allocated = true; + return *this; +} +Value& Value::operator=(const std::string& v) +{ + nullify(); + m_data.m_string = strdup(v.c_str()); + m_type = TYPE_STRING; + m_allocated = true; + return *this; +} +Value& Value::operator=(const DataArray& v) +{ + nullify(); + m_data.m_array = new DataArray(v); + m_type = TYPE_ARRAY; + m_allocated = true; + return *this; +} +Value& Value::operator=(const DataObject& v) +{ + nullify(); + m_data.m_object = new DataObject(v); + m_type = TYPE_OBJECT; + m_allocated = true; + return *this; +} + +void Value::assign(DataObject* obj) +{ + if (obj == nullptr) + { + m_type = TYPE_NULL; + m_allocated = false; + return; + } + nullify(); + m_type = TYPE_OBJECT; + m_allocated = true; + m_data.m_object = obj; +} +void Value::assign(DataArray* array) +{ + if (array == nullptr) + { + m_type = TYPE_NULL; + m_allocated = false; + return; + } + nullify(); + m_type = TYPE_ARRAY; + m_allocated = true; + m_data.m_array = array; +} +void Value::nullify() +{ + switch (m_type) + { + case TYPE_NULL: + break; + case TYPE_BOOLEAN: + break; + case TYPE_INTEGER: + break; + case TYPE_DOUBLE: + break; + case TYPE_STRING: + qcp_check(m_allocated); + free(m_data.m_string); + break; + case TYPE_ARRAY: + qcp_check(m_allocated); + delete m_data.m_array; + break; + case TYPE_OBJECT: + qcp_check(m_allocated); + delete m_data.m_object; + break; + } + m_type = TYPE_NULL; + m_allocated = false; +} + +Value::~Value() { this->nullify(); } + +int Value::to_int() const +{ + switch (m_type) + { + case TYPE_NULL: + return 0; + + case TYPE_BOOLEAN: + return int(m_data.m_bool); + + case TYPE_INTEGER: + return m_data.m_int; + + case TYPE_DOUBLE: + return int(m_data.m_double); + + case TYPE_STRING: + qcp_ensure(m_allocated); + qcp_ensure(m_data.m_string); + return atoi(m_data.m_string); + + case TYPE_ARRAY: + qcp_not_implemented("conversion of array to integer"); + + case TYPE_OBJECT: + qcp_not_implemented("conversion of object to integer") + } + qcp_not_implemented("unknown type conversion to integer") +} +double Value::to_double() const +{ + switch (m_type) + { + case TYPE_NULL: + return 0.0; + + case TYPE_BOOLEAN: + return double(m_data.m_bool); + + case TYPE_INTEGER: + return double(m_data.m_int); + + case TYPE_DOUBLE: + return m_data.m_double; + + case TYPE_STRING: + qcp_ensure(m_allocated); + qcp_ensure(m_data.m_string); + return atof(m_data.m_string); + + case TYPE_ARRAY: + qcp_not_implemented("conversion of array to double"); + + case TYPE_OBJECT: + qcp_not_implemented("conversion of object to double") + } + qcp_not_implemented("unknown type conversion to double") +} +bool Value::to_bool() const +{ + switch (m_type) + { + case TYPE_NULL: + return false; + + case TYPE_BOOLEAN: + return m_data.m_bool; + + case TYPE_INTEGER: + return m_data.m_int != 0; + + case TYPE_DOUBLE: + return m_data.m_double ? true : false; + + case TYPE_STRING: + qcp_not_implemented("conversion of string to boolean"); + + case TYPE_ARRAY: + qcp_not_implemented("conversion of array to double"); + + case TYPE_OBJECT: + qcp_not_implemented("conversion of object to double") + } + qcp_not_implemented("unknown type conversion to bool") +} +const char* Value::to_cstring() const +{ + switch (m_type) + { + case TYPE_NULL: + case TYPE_BOOLEAN: + case TYPE_INTEGER: + case TYPE_DOUBLE: + return nullptr; + case TYPE_STRING: + qcp_check(m_allocated); + return m_data.m_string; + + case TYPE_ARRAY: + qcp_not_implemented("conversion of array to cstring"); + + case TYPE_OBJECT: + qcp_not_implemented("conversion of object to cstring") + } + qcp_not_implemented("unknown type conversion to cstring") +} +std::string Value::to_string() const +{ + switch (m_type) + { + case TYPE_NULL: + return "null"; + case TYPE_BOOLEAN: + return m_data.m_bool ? "true" : "false"; + case TYPE_INTEGER: + return std::to_string(m_data.m_int); + case TYPE_DOUBLE: + return std::to_string(m_data.m_double); + case TYPE_STRING: + qcp_check(m_allocated); + return m_data.m_string; + + case TYPE_ARRAY: + qcp_not_implemented("conversion of array to string"); + + case TYPE_OBJECT: + std::stringstream str; + to_object()->pack_json(str); + return str.str(); + } + qcp_not_implemented("unknown type conversion to string") +} + +DataArray* Value::to_array() const +{ + qcp_insist( + convertable(TYPE_ARRAY), + "Value object must be convertable to an array") return m_data.m_array; +} +DataObject* Value::to_object() const +{ + qcp_insist( + convertable(TYPE_OBJECT), + "Value object must be convertable to an object") return m_data.m_object; +} + +const DataArray& Value::as_array() const +{ + qcp_insist( + convertable(TYPE_ARRAY), + "Value object must be convertable to an array") return *(m_data.m_array); +} + +DataArray& Value::as_array() +{ + qcp_insist( + convertable(TYPE_ARRAY), + "Value object must be convertable to an array") return *(m_data.m_array); +} + +const DataObject& Value::as_object() const +{ + qcp_insist( + convertable(TYPE_OBJECT), + "Value object must be convertable to an object") return *(m_data + .m_object); +} + +DataObject& Value::as_object() +{ + qcp_insist( + convertable(TYPE_OBJECT), + "Value object must be convertable to an object") return *(m_data + .m_object); +} +bool Value::convertable(Value::Type to) const +{ + switch (to) + { + case TYPE_NULL: + return (is_number() && to_double() == 0.0); + case TYPE_BOOLEAN: + case TYPE_INTEGER: + case TYPE_DOUBLE: + return is_bool() || is_null() || is_number(); + case TYPE_STRING: + return is_string(); + case TYPE_ARRAY: + return is_array(); + case TYPE_OBJECT: + return is_object(); + } + qcp_not_implemented("unknown type conversion") +} + +Value& Value::operator[](const std::string& name) +{ + qcp_check(is_object()); + DataObject* o = to_object(); + return o->operator[](name); +} +const Value& Value::operator[](const std::string& name) const +{ + qcp_check(is_object()); + const DataObject* o = to_object(); + return o->operator[](name); +} +Value& Value::operator[](size_t i) +{ + qcp_check(is_array()); + DataArray* a = to_array(); + return a->operator[](i); +} +const Value& Value::operator[](size_t i) const +{ + qcp_check(is_array()); + DataArray* a = to_array(); + return a->operator[](i); +} +bool Value::empty() const +{ + if (is_array()) + { + qcp_check(to_array()); + return to_array()->empty(); + } + if (is_object()) + { + qcp_check(to_object()); + return to_object()->empty(); + } + return is_null(); +} +size_t Value::size() const +{ + if (is_array()) + { + qcp_check(to_array()); + return to_array()->size(); + } + if (is_object()) + { + qcp_check(to_object()); + return to_object()->size(); + } + return 0; +} + +bool Value::format_json(std::ostream& out, int indent_level, int level, + bool order_by_insert) const +{ + if (is_object()) + { + to_object()->format_json(out, indent_level, level, order_by_insert); + } + else if (is_array()) + { + to_array()->format_json(out, indent_level, level, order_by_insert); + } + else + { + switch (type()) + { + case TYPE_STRING: + out << "\"" << to_string() << "\""; + break; + case TYPE_BOOLEAN: + out << std::boolalpha << to_bool(); + break; + case TYPE_INTEGER: + out << to_int(); + break; + case TYPE_DOUBLE: + // the precision can be set by the caller + out << to_double(); + break; + case TYPE_NULL: + out << "null"; + break; + default: + qcp_not_implemented("unknown Object value type json emission") + } + } + return out.good(); +} +bool Value::pack_json(std::ostream& out) const +{ + if (is_object()) + { + to_object()->pack_json(out); + } + else if (is_array()) + { + to_array()->pack_json(out); + } + else + { + switch (type()) + { + case TYPE_STRING: + out << "\"" << to_string() << "\""; + break; + case TYPE_BOOLEAN: + out << std::boolalpha << to_bool(); + break; + case TYPE_INTEGER: + out << to_int(); + break; + case TYPE_DOUBLE: + // the precision can be set by the caller + out << to_double(); + break; + case TYPE_NULL: + out << "null"; + break; + default: + qcp_not_implemented("unknown Object value type json emission") + } + } + return out.good(); +} +DataArray::DataArray() {} +DataArray::~DataArray() {} +DataArray::DataArray(const DataArray& orig) + : m_data(orig.m_data) +{ +} +size_t DataArray::size() const { return m_data.size(); } +bool DataArray::empty() const { return m_data.empty(); } +bool DataArray::format_json(std::ostream& out, int indent_level, int level, + bool order_by_insert) const +{ + qcp_require(indent_level >= 0); + qcp_require(level >= 0); + if (empty()) + { + out << "[]"; + return true; + } + out << "[" << std::endl; + std::string indent = std::string(indent_level * (level + 1), ' '); + out << indent; + at(0).format_json(out, indent_level, level + 1, order_by_insert); + + for (size_t i = 1, count = size(); i < count; ++i) + { + out << "," << std::endl << indent; + if (!at(i).format_json(out, indent_level, level + 1, order_by_insert)) + return false; + } + out << std::endl; + out << std::string(indent_level * level, ' ') << "]"; + return out.good(); +} +bool DataArray::pack_json(std::ostream& out) const +{ + if (empty()) + { + out << "[]"; + return true; + } + out << "["; + at(0).pack_json(out); + + for (size_t i = 1, count = size(); i < count; ++i) + { + out << ","; + if (!at(i).pack_json(out)) return false; + } + out << "]"; + return out.good(); +} + +void DataArray::merge(const DataArray& rhs) +{ + // loop over every item in rhs array + size_t index = 0; + for (auto item : rhs) + { + qcp_line("Checking inbound array index(" << index << ")"); + if (size() <= index) + { + qcp_line("lhs doesn't have index. adding rhs index to lhs"); + this->push_back(item); + } + else + { + // must check the type of lhs[index] against that of rhs[index] + Value& child = m_data.at(index); + // if Value is an object do a depth-first merge + if (child.is_object()) + { + // check that my object is indeed an object + if (!item.is_object()) + { + std::stringstream ss; + ss << "Attempting to merge data arrary where member index(" << index + << ") differs in type." << std::endl + << "Left-hand side type(object) differs from right-hand " + "side type(" + << item.categoryString() << ")"; + throw std::logic_error(ss.str()); + } + // merge objects + child.to_object()->merge(item.as_object()); + } + else if (child.is_array()) + { // if Value is an array do a depth-first merge + if (!item.is_array()) + { + std::stringstream ss; + ss << "Attempting to merge data array where member index(" << index + << " differs in type." + << "Left-hand side type(array) differs from right-hand " + "side type(" + << item.categoryString() << ")"; + throw std::logic_error(ss.str()); + } + // merge array + child.to_array()->merge(item.as_array()); + } + } + ++index; + } +} +DataObject::DataObject() {} +DataObject::DataObject(const DataObject& orig) + : m_data(orig.m_data) + , m_insert_order(orig.m_insert_order) +{ +} + +DataObject::~DataObject() {} +size_t DataObject::size() const { return m_data.size(); } +bool DataObject::empty() const { return m_data.empty(); } + +Value& DataObject::operator[](const std::string& name) +{ + // since c++11 std::map<>[] does insertion if key doesn't exist + m_insert_order.push_back(name); + return m_data[name]; +} + +const Value& DataObject::operator[](const std::string& name) const +{ + auto itr = m_data.find(name); + + if (itr == m_data.end()) + { + throw std::out_of_range(name + " not found in object"); + } + return itr->second; +} + +std::pair<DataObject::storage_type::iterator, bool> DataObject::insert( + const std::pair<std::string, Value>& v) +{ + m_insert_order.push_back(v.first); + return m_data.insert(v); +} + +bool DataObject::format_json(std::ostream& out, int indent_level, int level, + bool order_by_insert) const +{ + qcp_require(indent_level >= 0); + qcp_check(level >= 0); + if (empty()) + { + out << "{}"; + return true; + } + + out << "{" << std::endl; + std::string indent = std::string(indent_level * (level + 1), ' '); + out << indent; + + // determine order to print (map default of sorted, or order of insertion) + std::vector<std::string> order; + if (order_by_insert) + order = m_insert_order; + else + for (auto itr = m_data.begin(); itr != m_data.end(); ++itr) + order.push_back(itr->first); + + auto itr = order.begin(); + out << "\"" << *itr << "\" : "; + m_data.at(*itr).format_json(out, indent_level, level + 1, order_by_insert); + ++itr; + for (; itr != order.end(); ++itr) + { + out << "," << std::endl; + out << indent << "\"" << *itr << "\" : "; + if (!m_data.at(*itr).format_json(out, indent_level, level + 1, + order_by_insert)) + return false; + } + + out << std::endl; + + out << std::string(indent_level * (level), ' ') << "}"; + return out.good(); +} +bool DataObject::pack_json(std::ostream& out) const +{ + if (empty()) + { + out << "{}"; + return true; + } + + out << "{"; + auto itr = begin(); + out << "\"" << itr->first << "\":"; + itr->second.pack_json(out); + ++itr; + for (; itr != end(); ++itr) + { + out << ","; + out << "\"" << itr->first << "\":"; + if (!itr->second.pack_json(out)) return false; + } + + out << "}"; + return out.good(); +} + +void DataObject::merge(const DataObject& rhs) +{ + // merge every Value, Object and Array from 'obj' + // that does not exist in 'this' + for (auto& item : rhs) + { + qcp_line("Checking inbound " << item.first); + if (contains(item.first)) + { + qcp_line("Destination contains " << item.first); + Value& child = m_data.at(item.first); + // if Value is an object do a depth-first merge + if (child.is_object()) + { + // check that my object is indeed an object + if (!item.second.is_object()) + { + std::stringstream ss; + ss << "Attempting to merge data objects where member(" << item.first + << ") differs in type." << std::endl + << "Left-hand side type(object) differs from right-hand " + "side type(" + << item.second.categoryString() << ")"; + throw std::logic_error(ss.str()); + } + // merge objects + child.to_object()->merge(item.second.as_object()); + } + else if (child.is_array()) + { // if Value is an array do a depth-first merge + if (!item.second.is_array()) + { + std::stringstream ss; + ss << "Attempting to merge data objects where member(" << item.first + << " differs in type." + << "Left-hand side type(array) differs from right-hand " + "side type(" + << item.second.categoryString() << ")"; + throw std::logic_error(ss.str()); + } + // merge array + child.to_array()->merge(item.second.as_array()); + } + } + else + { // no destination to merge so we can simply copy + qcp_line("Destination does not contain " << item.first + << ". Copying..."); + this->insert(item); + } + } +} +} // namespace qcp diff --git a/qcpcore/value.hh b/qcpcore/value.hh new file mode 100644 index 0000000000000000000000000000000000000000..3e48424efd2054462919350d5aba0f67b529dac1 --- /dev/null +++ b/qcpcore/value.hh @@ -0,0 +1,256 @@ +#ifndef QCP_QCPCORE_VALUE_HH_ +#define QCP_QCPCORE_VALUE_HH_ + +#include <map> +#include <memory> +#include <string> +#include <vector> + +#include "qcpcore/declspec.hh" + +namespace qcp +{ +/** + * @brief The Value class represents values of objects (null, integer, double, + * string, array, or object) + */ +class QCP_PUBLIC Value +{ + public: + typedef std::shared_ptr<Value> SP; + enum Type : unsigned char + { + TYPE_NULL, + TYPE_BOOLEAN, + TYPE_INTEGER, + TYPE_DOUBLE, + TYPE_STRING, + TYPE_ARRAY, + TYPE_OBJECT + }; + + private: + bool m_allocated; + Type m_type; + union DataUnion { + bool m_bool; + double m_double; + int m_int; + char* m_string; + class DataArray* m_array; + class DataObject* m_object; + } m_data; + + public: + /// null constructor + Value(); + /// copy constructor + Value(const Value& orig); + // move constructor + Value(Value&& orig); + + // boolean + Value(bool v); + // integer + Value(int v); + // double + Value(double v); + // const char * + Value(const char* v); + // string + Value(const std::string& v); + // data array + Value(const DataArray& v); + // data object + Value(const DataObject& v); + ~Value(); + + // assignment operators + Value& operator=(const Value& orig); + Value& operator=(Value&& orig); + Value& operator=(bool v); + Value& operator=(int v); + Value& operator=(double v); + Value& operator=(const char* v); + Value& operator=(const std::string& v); + Value& operator=(const DataArray& v); + Value& operator=(const DataObject& v); + + Value::Type type() const; + /** + * @brief categoryString + * @return Returns a string for type display (object, array, number, + * boolean, null) + */ + std::string categoryString() const; + + bool is_null() const { return m_type == TYPE_NULL; } + bool is_int() const { return m_type == TYPE_INTEGER; } + bool is_double() const { return m_type == TYPE_DOUBLE; } + bool is_number() const { return is_int() || is_double(); } + bool is_bool() const { return m_type == TYPE_BOOLEAN; } + bool is_string() const { return m_type == TYPE_STRING; } + bool is_array() const { return m_type == TYPE_ARRAY; } + bool is_object() const { return m_type == TYPE_OBJECT; } + bool is_primitive() const { return !(is_array() || is_object()); } + + bool convertable(Value::Type to) const; + + int to_int() const; + double to_double() const; + bool to_bool() const; + const char* to_cstring() const; + std::string to_string() const; + DataArray* to_array() const; + DataObject* to_object() const; + + const DataArray& as_array() const; + DataArray& as_array(); + const DataObject& as_object() const; + DataObject& as_object(); + + Value& operator[](const std::string& name); + const Value& operator[](const std::string& name) const; + Value& operator[](size_t i); + const Value& operator[](size_t i) const; + + /** + * @brief empty whether the value is empty + * @return false when value is null or empty object/array + */ + bool empty() const; + + /** + * @brief size the number of elements (object keys or array indices) + * @return size_t array element count, object member count, or 0 + */ + size_t size() const; + + bool format_json(std::ostream& out, int indent_level = 2, int level = 0, + bool order_by_insert = false) const; + bool pack_json(std::ostream& out) const; + + private: + friend class JSONObjectParser; + void assign(DataObject* obj); + void assign(DataArray* array); + /** + * @brief nullify deletes and nullifies this object + */ + void nullify(); + + /** + * @brief copy_from copies the given value to this value + * @param orig the value from which data will be copied + */ + void copy_from(const Value& orig); +}; + +class QCP_PUBLIC DataArray +{ + public: + typedef std::shared_ptr<DataArray> SP; + typedef std::vector<Value> storage_type; + + private: + storage_type m_data; + + public: + DataArray(); + DataArray(const DataArray& orig); + ~DataArray(); + + size_t size() const; + bool empty() const; + + storage_type::const_iterator begin() const { return m_data.begin(); } + storage_type::const_iterator end() const { return m_data.end(); } + storage_type::iterator begin() { return m_data.begin(); } + storage_type::iterator end() { return m_data.end(); } + + const Value& operator[](size_t i) const + { + return m_data.at(i); + } // at(i) for exception + Value& operator[](size_t i) + { + if (size() <= i) resize(i + 1); + return m_data[i]; + } + + const Value& front() const { return m_data.front(); } + Value& front() { return m_data.front(); } + const Value& back() const { return m_data.back(); } + Value& back() { return m_data.back(); } + Value& at(size_t i) { return m_data.at(i); } + const Value& at(size_t i) const { return m_data.at(i); } + void push_back(const Value& n) { m_data.push_back(n); } + void resize(size_t nsize) { m_data.resize(nsize); } + + bool format_json(std::ostream& out, int indent_level = 2, int level = 0, + bool order_by_insert = false) const; + bool pack_json(std::ostream& out) const; + void merge(const DataArray& rhs); +}; + +class QCP_PUBLIC DataObject +{ + public: + typedef std::shared_ptr<DataObject> SP; + typedef std::map<std::string, Value> storage_type; + + private: + std::vector<std::string> m_insert_order; + storage_type m_data; + + public: + DataObject(); + DataObject(const DataObject& orig); + ~DataObject(); + + size_t size() const; + bool empty() const; + + storage_type::const_iterator find(const std::string& name) const + { + return m_data.find(name); + } + storage_type::iterator find(const std::string& name) + { + return m_data.find(name); + } + + storage_type::const_iterator begin() const { return m_data.begin(); } + storage_type::const_iterator end() const { return m_data.end(); } + + storage_type::iterator begin() { return m_data.begin(); } + storage_type::iterator end() { return m_data.end(); } + + Value& operator[](const std::string& name); + const Value& operator[](const std::string& name) const; + std::pair<storage_type::iterator, bool> insert( + const std::pair<std::string, Value>& v); + + bool contains(const std::string& name) const + { + return m_data.find(name) != end(); + } + + bool format_json(std::ostream& out, int indent_level = 2, int level = 0, + bool order_by_insert = false) const; + bool pack_json(std::ostream& out) const; + + void merge(const DataObject& rhs); +}; + +template <class Interp> +QCP_PUBLIC bool generate_object(DataObject::SP& obj, std::istream& input, + std::ostream& errors) +{ + Interp interpreter(obj, input, errors, nullptr); + bool parsed = interpreter.parse() == 0; + return parsed; +} + +} // namespace qcp +#endif /** QCP_QCPCORE_VALUE_HH_ */ diff --git a/qcpexchange/cmake/Dependencies.cmake b/qcpexchange/cmake/Dependencies.cmake index 9e4d55fe863831c5d4609bcb2f50144b7dbee5d9..de696c74a63ffaaf81dbd4a6aa80d82c07ea1459 100644 --- a/qcpexchange/cmake/Dependencies.cmake +++ b/qcpexchange/cmake/Dependencies.cmake @@ -3,7 +3,7 @@ ## Tom Norby ##---------------------------------------------------------------------------## TRIBITS_PACKAGE_DEFINE_DEPENDENCIES( - LIB_REQUIRED_PACKAGES qcpcontext radixwidgets radixcore radixbug + LIB_REQUIRED_PACKAGES qcpcontext LIB_REQUIRED_TPLS Qt5Widgets Qt5Core Qt5Network ) ##---------------------------------------------------------------------------## diff --git a/qcpexchange/qcpexchange.cc b/qcpexchange/qcpexchange.cc index a597d914d53afdadf4f803e6b17748cd8eea98d6..f092135d0ce0856078c50b4414455e7f437ef090 100644 --- a/qcpexchange/qcpexchange.cc +++ b/qcpexchange/qcpexchange.cc @@ -1,13 +1,13 @@ #include "qcpexchange/qcpexchange.hh" #include "qcpexchange/singleinstanceapp.hh" -#include "radixbug/bug.hh" -#include "radixcore/json.hh" -#include "radixcore/system.hh" +#include "qcpcore/bug.hh" +#include "qcpcore/json.hh" +//#include "qcpcore/system.hh" -#include "radixwidgets/navigationactionmanager.hh" -#include "radixwidgets/navigationitem.hh" -#include "radixwidgets/navigationmodel.hh" +#include "qcpcontext/navigationactionmanager.hh" +#include "qcpcontext/navigationitem.hh" +#include "qcpcontext/navigationmodel.hh" #include <QApplication> #include <QLocalSocket> @@ -24,8 +24,8 @@ QCPExchange::QCPExchange(QWidget *parent) mSplit = new QSplitter(mCentralWidget); - mNavigationView = new radix::NavigationWidget(this); - radix::NavigationModel *navModel = mNavigationView->navigationModel(); + mNavigationView = new NavigationWidget(this); + NavigationModel *navModel = mNavigationView->navigationModel(); // mPlotTreeView = new QTreeView(this); // mPlotTreeModel = new QStandardItemModel(mPlotTreeView); // mPlotTreeView->setModel(mPlotTreeModel); @@ -52,7 +52,7 @@ QCPExchange::QCPExchange(QWidget *parent) { if (args.length() < 2) { - radix_line("No json file passed to application."); + qcp_line("No json file passed to application."); return; } @@ -72,7 +72,7 @@ QCPExchange::QCPExchange(QWidget *parent) } }; - connect(mPlot->qcp(), SIGNAL(selectionChangedByUser()), this, + connect(mPlot->qcplot(), SIGNAL(selectionChangedByUser()), this, SLOT(plotSelectionChangedByUser())); // // connect navigation model signals @@ -118,34 +118,35 @@ QCPExchange::~QCPExchange() {} void QCPExchange::processPlotFile(const char *absFilepath) { - radix::NavigationModel *navModel = mNavigationView->navigationModel(); - radix::NavigationActionManager *navActions = + NavigationModel *navModel = mNavigationView->navigationModel(); + NavigationActionManager *navActions = mNavigationView->navigationActionManager(); // get plot data from .json - radix::DataObject rootObject; + DataObject rootObject; { - radix::JSONParser jp; + JSONParser jp; bool parsed = jp.parse_from_file(absFilepath); if (!parsed || jp.root().is_null()) { - radix_line("JSONParser failed to parse'" << absFilepath << "'"); - radix_line("JSONParser ERROR: " << jp.last_error()); + qcp_line("JSONParser failed to parse'" << absFilepath << "'"); + qcp_line("JSONParser ERROR: " << jp.last_error()); QMessageBox::information(this, "Error", jp.last_error().c_str()); return; } if (!jp.root().is_object()) { - radix_line("Root is not an object"); + qcp_line("Root is not an object"); return; } rootObject = jp.root().as_object(); } - radix_line("Successfully parsed JSON from: " << absFilepath); + qcp_line("Successfully parsed JSON from: " << absFilepath); + QFileInfo fileInfo(absFilepath); // // Add root file item to navigation model - radix::NavigationItem *navFileItem = navModel->addItem( - radix::basename(absFilepath).c_str(), navModel->rootItem()); + NavigationItem *navFileItem = navModel->addItem( + fileInfo.baseName(), navModel->rootItem()); // Display absolute file path on mouse over navFileItem->setData(0, absFilepath, Qt::ToolTipRole); // @@ -163,10 +164,10 @@ void QCPExchange::processPlotFile(const char *absFilepath) // interactions if (rootObject.contains("interactions")) { - const radix::DataArray &interactionArray = + const DataArray &interactionArray = rootObject["interactions"].as_array(); QCP::Interactions interactions = nullptr; - for (const radix::Value &val : interactionArray) + for (const Value &val : interactionArray) { QCP::Interaction interaction = QCP::Interaction(val.to_int()); if (!interactions) @@ -174,14 +175,14 @@ void QCPExchange::processPlotFile(const char *absFilepath) else interactions |= interaction; } - mPlot->qcp()->setInteractions(interactions); + mPlot->qcplot()->setInteractions(interactions); } // selection rect mode if (rootObject.contains("selectionRectMode")) { int srm = rootObject["selectionRectMode"].to_int(); - mPlot->qcp()->setSelectionRectMode(QCP::SelectionRectMode(srm)); + mPlot->qcplot()->setSelectionRectMode(QCP::SelectionRectMode(srm)); } struct AxisStruct @@ -193,7 +194,7 @@ void QCPExchange::processPlotFile(const char *absFilepath) int numberPrecision; }; auto axisOptionsLambda = [](AxisStruct &axis, - const radix::DataObject &axisVal) { + const DataObject &axisVal) { if (axisVal.contains("label")) axis.label = axisVal["label"].to_cstring(); if (axisVal.contains("subGridVisible")) @@ -235,13 +236,13 @@ void QCPExchange::processPlotFile(const char *absFilepath) // graphs { - const radix::DataArray &groups = rootObject["groups"].as_array(); + const DataArray &groups = rootObject["groups"].as_array(); for (size_t gi = 0; gi < groups.size(); ++gi) { - const radix::DataObject &group = groups[gi].as_object(); + const DataObject &group = groups[gi].as_object(); // // Add group to the file navigation - radix::NavigationItem *plotGroupNav = + NavigationItem *plotGroupNav = navModel->addItem(group["groupName"].to_cstring(), navFileItem); // // Register actions to group navigation @@ -251,12 +252,12 @@ void QCPExchange::processPlotFile(const char *absFilepath) navActions->clearActions(plotGroupNav); }); - const radix::DataArray &graphsArray = group["graphs"].as_array(); + const DataArray &graphsArray = group["graphs"].as_array(); for (size_t i = 0; i < graphsArray.size(); ++i) { QCPGraphData::SP graphData = std::make_shared<QCPGraphData>(); // cast as an object - const radix::DataObject &graph = graphsArray[i].as_object(); + const DataObject &graph = graphsArray[i].as_object(); if (graph.contains("keyAxis") && graph.contains("valueAxis")) { std::string value = graph["keyAxis"].to_string(); @@ -286,7 +287,7 @@ void QCPExchange::processPlotFile(const char *absFilepath) graphData->setName(graph["name"].to_cstring()); } - radix::NavigationItem *plotItemNav = + NavigationItem *plotItemNav = navModel->addItem(graphData->name(), plotGroupNav); navActions->registerAction(plotItemNav, "remove", @@ -302,7 +303,7 @@ void QCPExchange::processPlotFile(const char *absFilepath) // pen color & style if (graph.contains("pen")) { - const radix::DataObject &pen = graph["pen"].as_object(); + const DataObject &pen = graph["pen"].as_object(); if (pen.contains("color")) { graphData->setPenColor(pen["color"].to_cstring()); @@ -340,13 +341,13 @@ void QCPExchange::processPlotFile(const char *absFilepath) if (graph.contains("xData") && graph.contains("yData")) { QVector<double> xData, yData; - const radix::DataArray &xArray = graph["xData"].as_array(); - for (const radix::Value &val : xArray) + const DataArray &xArray = graph["xData"].as_array(); + for (const Value &val : xArray) { xData.push_back(val.to_double()); } - const radix::DataArray &yArray = graph["yData"].as_array(); - for (const radix::Value &val : yArray) + const DataArray &yArray = graph["yData"].as_array(); + for (const Value &val : yArray) { yData.push_back(val.to_double()); } @@ -361,8 +362,8 @@ void QCPExchange::processPlotFile(const char *absFilepath) bool QCPExchange::enableItem(const QModelIndex &index) { - radix::NavigationModel *navModel = mNavigationView->navigationModel(); - radix::NavigationItem *item = navModel->toItem(index); + NavigationModel *navModel = mNavigationView->navigationModel(); + NavigationItem *item = navModel->toItem(index); auto it = mData.find(item); // not all nav items have graph data associated if (it != mData.end()) @@ -371,7 +372,7 @@ bool QCPExchange::enableItem(const QModelIndex &index) QCPGraph *graph = it->second.second; // if the graph is already allocated, return if (graph != nullptr) return true; - radix_tagged_line("Item checked (" << item << ")->name(" + qcp_tagged_line("Item checked (" << item << ")->name(" << data->name().toStdString() << ")"); QCPAxis *keyAxis = nullptr, *valueAxis = nullptr; QString value = data->keyAxisName(); @@ -412,14 +413,14 @@ bool QCPExchange::enableItem(const QModelIndex &index) QString xAxisLabel = data->xAxisLabel(); QString yAxisLabel = data->yAxisLabel(); - radix_tagged_line("Comparing xAxis(" + qcp_tagged_line("Comparing xAxis(" << keyAxis->label().toStdString() << ") to data(" << xAxisLabel.toStdString() << ") and yAxis(" << valueAxis->label().toStdString() << ") to data(" << yAxisLabel.toStdString() << ")"); if ((xAxisLabel.compare(keyAxis->label()) != 0 || yAxisLabel.compare(valueAxis->label()) != 0) && - (mPlot->qcp()->plottableCount() != 0)) // There are plots visible + (mPlot->qcplot()->plottableCount() != 0)) // There are plots visible { // // Ask the user if they want to continue @@ -493,14 +494,14 @@ bool QCPExchange::enableItem(const QModelIndex &index) void QCPExchange::plotSelectionChangedByUser() { - int graphCount = mPlot->qcp()->graphCount(); + int graphCount = mPlot->qcplot()->graphCount(); // // Select the item in the legend if the graph was selected within context for (int i = 0; i < graphCount; ++i) { - QCPGraph *graph = mPlot->qcp()->graph(i); + QCPGraph *graph = mPlot->qcplot()->graph(i); QCPPlottableLegendItem *item = - mPlot->qcp()->legend->itemWithPlottable(graph); + mPlot->qcplot()->legend->itemWithPlottable(graph); if (item->selected() || graph->selected()) { item->setSelected(true); @@ -541,9 +542,9 @@ void QCPExchange::saveToCSV(QCPContext *plot) // Create file std::ofstream ofs(filePath.toStdString(), std::ofstream::out); - for (int i = 0; i < plot->qcp()->graphCount(); ++i) + for (int i = 0; i < plot->qcplot()->graphCount(); ++i) { - QCPGraph *graph = plot->qcp()->graph(i); + QCPGraph *graph = plot->qcplot()->graph(i); // print header ofs << graph->name().toStdString() << ", " << graph->keyAxis()->label().toStdString() << ", " @@ -557,7 +558,7 @@ void QCPExchange::saveToCSV(QCPContext *plot) void QCPExchange::itemChecked(const QModelIndex &index) { - radix::NavigationModel *navModel = mNavigationView->navigationModel(); + NavigationModel *navModel = mNavigationView->navigationModel(); if (index.isValid()) { @@ -591,10 +592,10 @@ void QCPExchange::itemChecked(const QModelIndex &index) void QCPExchange::itemUnChecked(const QModelIndex &index) { - radix_tagged_line("item unchecked"); + qcp_tagged_line("item unchecked"); if (index.isValid()) { - radix::NavigationModel *navModel = mNavigationView->navigationModel(); + NavigationModel *navModel = mNavigationView->navigationModel(); int numKids = navModel->rowCount(index); for (int ki = 0; ki < numKids; ++ki) @@ -604,8 +605,8 @@ void QCPExchange::itemUnChecked(const QModelIndex &index) // set them as checked navModel->setData(child, false, Qt::CheckStateRole); } - radix::NavigationItem *item = navModel->toItem(index); - radix_tagged_line("Item unchecked (" << item << ")"); + NavigationItem *item = navModel->toItem(index); + qcp_tagged_line("Item unchecked (" << item << ")"); auto it = mData.find(item); // not all nav items have graph data associated if (it != mData.end()) @@ -613,7 +614,7 @@ void QCPExchange::itemUnChecked(const QModelIndex &index) QCPGraphData::SP data = it->second.first; QCPGraph *graph = it->second.second; if (graph == nullptr) return; - mPlot->qcp()->removeGraph(graph); + mPlot->qcplot()->removeGraph(graph); it->second.second = nullptr; updatePlot(true); } @@ -625,8 +626,8 @@ static bool updateFile() { QLocalSocket *socket = new QLocalSocket; socket->connectToServer("localhost"); - radix_line(socket->error()); - radix_line(socket->open()); + qcp_line(socket->error()); + qcp_line(socket->open()); QDir appdir = SingleInstanceApp::applicationDirPath(); QStringList args = SingleInstanceApp::arguments(); QByteArray block; diff --git a/qcpexchange/qcpexchange.hh b/qcpexchange/qcpexchange.hh index 592957efd1da4b0365db3429a1082e6ee762009b..1c0469ba248754095c9fc30458e30d4f8d354b58 100644 --- a/qcpexchange/qcpexchange.hh +++ b/qcpexchange/qcpexchange.hh @@ -11,8 +11,8 @@ #include "qcpcontext/qcpgraphdata.hh" #include "qcpexchange/plottreeitem.hh" -#include "radixwidgets/navigationitem.hh" -#include "radixwidgets/navigationwidget.hh" +#include "qcpcontext/navigationitem.hh" +#include "qcpcontext/navigationwidget.hh" namespace qcp { @@ -44,8 +44,8 @@ class QCPExchange : public QMainWindow QSplitter *mSplit; QVBoxLayout *mPlotLayout; - radix::NavigationWidget *mNavigationView; - std::unordered_map<radix::NavigationItem *, + NavigationWidget *mNavigationView; + std::unordered_map<NavigationItem *, std::pair<QCPGraphData::SP, QCPGraph *>> mData; diff --git a/submodules/radix b/submodules/radix deleted file mode 160000 index 582e91f5a30cc25b4cd58fc889dc1aa32f5e5c2a..0000000000000000000000000000000000000000 --- a/submodules/radix +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 582e91f5a30cc25b4cd58fc889dc1aa32f5e5c2a