From 3c5e651b782df6429f7b128f1d8157351af3ca80 Mon Sep 17 00:00:00 2001 From: Mark Janes Date: Fri, 20 Oct 2017 19:53:48 -0700 Subject: State: Display a tree of collapsible state directories This implementation of a custom tree view is similar to Qt's native implementation: a modified ListView that hides items based on whether a parent directory is collapsed. --- retrace/daemon/glframe_retrace_interface.hpp | 8 +- retrace/daemon/glframe_retrace_render.cpp | 5 +- retrace/daemon/ui/glframe_state_model.cpp | 141 +++++++++++++++++++++++---- retrace/daemon/ui/glframe_state_model.hpp | 38 ++++++-- retrace/daemon/ui/qml/StateControl.qml | 37 ++++++- 5 files changed, 198 insertions(+), 31 deletions(-) diff --git a/retrace/daemon/glframe_retrace_interface.hpp b/retrace/daemon/glframe_retrace_interface.hpp index 61fba458..5b1fba45 100644 --- a/retrace/daemon/glframe_retrace_interface.hpp +++ b/retrace/daemon/glframe_retrace_interface.hpp @@ -270,14 +270,14 @@ struct StateKey { const std::string _name) : group(_group), path(_path), name(_name) {} bool operator<(const StateKey &o) const { - if (name < o.name) - return true; - if (name > o.name) - return false; if (path < o.path) return true; if (path > o.path) return false; + if (name < o.name) + return true; + if (name > o.name) + return false; return group < o.group; } }; diff --git a/retrace/daemon/glframe_retrace_render.cpp b/retrace/daemon/glframe_retrace_render.cpp index dcb73284..a0ebfff1 100644 --- a/retrace/daemon/glframe_retrace_render.cpp +++ b/retrace/daemon/glframe_retrace_render.cpp @@ -581,7 +581,7 @@ RetraceRender::onState(SelectionId selId, GLenum e = GL::GetError(); if (e == GL_NO_ERROR) { callback->onState(selId, experimentCount, renderId, - StateKey("", "", "CULL_FACE"), + StateKey("Rendering", "Cull State", "CULL_FACE"), cull_enabled ? "true" : "false"); } } @@ -594,7 +594,8 @@ RetraceRender::onState(SelectionId selId, const std::string cull_str = value_to_string(cull); if (cull_str.size() > 0) { callback->onState(selId, experimentCount, renderId, - StateKey("", "", "CULL_FACE_MODE"), cull_str); + StateKey("Rendering", "Cull State", + "CULL_FACE_MODE"), cull_str); } } } diff --git a/retrace/daemon/ui/glframe_state_model.cpp b/retrace/daemon/ui/glframe_state_model.cpp index da0063e7..741643f1 100644 --- a/retrace/daemon/ui/glframe_state_model.cpp +++ b/retrace/daemon/ui/glframe_state_model.cpp @@ -28,6 +28,7 @@ #include "glframe_state_model.hpp" #include +#include #include #include @@ -41,12 +42,27 @@ using glretrace::state_name_to_enum; static const int kUninitializedValue = -2; static const int kMixedValue = -1; -QStateValue::QStateValue(const std::string &_name, +QStateValue::QStateValue(QObject *parent) { + if (parent) + moveToThread(parent->thread()); +} + +QStateValue::QStateValue(QObject *parent, + const std::string &_group, + const std::string &_path, + const std::string &_name, const std::vector &_choices) - : m_name(_name.c_str()), - m_value(kUninitializedValue) { + : m_group(_group.c_str()), + m_path(_path.c_str()), + m_name(_name.c_str()), + m_value(kUninitializedValue), + m_visible(true) { + moveToThread(parent->thread()); for (auto c : _choices) m_choices.append(QVariant(c.c_str())); + m_indent = static_cast(std::count(_path.begin(), _path.end(), '/')); + if (_name.length() == 0) + m_name = _path.substr(_path.find_last_of("/") + 1).c_str(); } void @@ -113,10 +129,12 @@ QStateModel::clear() { emit stateChanged(); { ScopedLock s(m_protect); - for (auto i : m_state_by_name) - delete i.second; m_state_by_name.clear(); m_renders.clear(); + m_known_paths.clear(); + for (auto i : m_for_deletion) + delete i; + m_for_deletion.clear(); } } @@ -126,13 +144,7 @@ void QStateModel::onState(SelectionId selectionCount, StateKey item, const std::string &value) { if (selectionCount == SelectionId(SelectionId::INVALID_SELECTION)) { - { - ScopedLock s(m_protect); - m_states.clear(); - for (auto i : m_state_by_name) - m_states.push_back(i.second); - } - emit stateChanged(); + refresh(); return; } @@ -151,19 +163,44 @@ void QStateModel::onState(SelectionId selectionCount, } if (m_renders.empty() || renderId != m_renders.back()) m_renders.push_back(renderId); + std::string path_comp = item.path; + while (path_comp.length() > 0) { + auto known = m_known_paths.find(path_comp); + if (known == m_known_paths.end()) { + // create an empty item to serve as the directory + QStateValue *i = new QStateValue(this, + item.group, + path_comp, + "", + std::vector()); + StateKey k(item.group, path_comp, ""); + m_state_by_name[k] = i; + m_known_paths[item.path] = true; + } else { + break; + } + path_comp = path_comp.substr(0, path_comp.find_last_of("/")); + } + auto &name = item.name; - auto state_value = m_state_by_name.find(name); + auto state_value = m_state_by_name.find(item); if (state_value == m_state_by_name.end()) { - QStateValue *i = new QStateValue(name, + QStateValue *i = new QStateValue(this, + item.group, + item.path, + name, name_to_choices(name)); - m_state_by_name[name] = i; - state_value = m_state_by_name.find(name); + m_state_by_name[item] = i; + state_value = m_state_by_name.find(item); + m_for_deletion.push_back(i); } state_value->second->insert(value); } void -QStateModel::setState(const QString &name, +QStateModel::setState(const QString &group, + const QString &path, + const QString &name, const QString &value) { RenderSelection sel; sel.id = m_sel_count; @@ -178,8 +215,76 @@ QStateModel::setState(const QString &name, ++r; } - StateKey key("", "", name.toStdString()); + StateKey key(group.toStdString(), path.toStdString(), name.toStdString()); m_retrace->setState(sel, key, value.toStdString()); emit stateExperiment(); } +void +QStateModel::collapse(const QString &path) { + const std::string path_str = path.toStdString(); + m_filter_paths[path_str] = true; + for (auto i : m_state_by_name) { + if (!i.second->visible().toBool()) + continue; + if (strncmp(path_str.c_str(), i.first.path.c_str(), + path_str.length()) == 0) { + // do not filter the collapsed directories themselves + if ((i.first.path == path_str) && (i.first.name.length() == 0)) + continue; + i.second->setVisible(false); + } + } +} + +void +QStateModel::expand(const QString &path) { + const std::string path_str = path.toStdString(); + auto i = m_filter_paths.find(path_str); + assert(i != m_filter_paths.end()); + m_filter_paths.erase(i); + for (auto i : m_state_by_name) { + if (i.second->visible().toBool()) + continue; + if (strncmp(path_str.c_str(), i.first.path.c_str(), + path_str.length()) == 0) { + // possibly expanded + bool visible = true; + for (auto f : m_filter_paths) { + if (strncmp(f.first.c_str(), i.first.path.c_str(), + f.first.length()) == 0) { + // do not filter the collapsed directories themselves + if ((i.first.path == f.first) && (i.first.name.length() == 0)) + visible = true; + else + visible = false; + } + } + i.second->setVisible(visible); + } + } +} + +void +QStateModel::refresh() { + { + ScopedLock s(m_protect); + m_states.clear(); + for (auto i : m_state_by_name) { + bool visible = true; + for (auto f : m_filter_paths) { + if (strncmp(f.first.c_str(), i.first.path.c_str(), + f.first.length()) == 0) { + // do not filter the collapsed directories themselves + if ((i.first.path == f.first) && (i.first.name.length() == 0)) + visible = true; + else + visible = false; + } + } + i.second->setVisible(visible); + m_states.push_back(i.second); + } + } + emit stateChanged(); +} diff --git a/retrace/daemon/ui/glframe_state_model.hpp b/retrace/daemon/ui/glframe_state_model.hpp index 6c764382..a08fd440 100644 --- a/retrace/daemon/ui/glframe_state_model.hpp +++ b/retrace/daemon/ui/glframe_state_model.hpp @@ -47,23 +47,41 @@ namespace glretrace { class QStateValue : public QObject, NoCopy, NoAssign, NoMove { Q_OBJECT + Q_PROPERTY(QString group READ group CONSTANT) + Q_PROPERTY(QString path READ path CONSTANT) Q_PROPERTY(QString name READ name CONSTANT) Q_PROPERTY(QVariant value READ value CONSTANT) + Q_PROPERTY(QVariant indent READ indent CONSTANT) + Q_PROPERTY(QVariant visible + READ visible + WRITE setVisible + NOTIFY visibleChanged) Q_PROPERTY(QList choices READ choices CONSTANT) public: - QStateValue() {} - QStateValue(const std::string &_name, + explicit QStateValue(QObject *parent = 0); + QStateValue(QObject *parent, + const std::string &_group, + const std::string &_path, + const std::string &_name, const std::vector &_choices); void insert(const std::string &value); + QString group() const { return m_group; } + QString path() const { return m_path; } QString name() const { return m_name; } QVariant value() const { return m_value; } + QVariant indent() const { return m_indent; } + QVariant visible() const { return m_visible; } + void setVisible(QVariant v) { m_visible = v; emit visibleChanged(); } QList choices() const { return m_choices; } + signals: + void visibleChanged(); + private: - QString m_name; - QVariant m_value; + QString m_group, m_path, m_name; + QVariant m_value, m_indent, m_visible; QList m_choices; }; @@ -83,19 +101,27 @@ class QStateModel : public QObject, StateKey item, const std::string &value); void clear(); - Q_INVOKABLE void setState(const QString &name, + Q_INVOKABLE void setState(const QString &group, + const QString &path, + const QString &name, const QString &value); + Q_INVOKABLE void collapse(const QString &path); + Q_INVOKABLE void expand(const QString &path); signals: void stateExperiment(); void stateChanged(); private: + void refresh(); + IFrameRetrace *m_retrace; SelectionId m_sel_count; ExperimentId m_experiment_count; // typedef QList StateList; - std::map m_state_by_name; + std::map m_state_by_name; + std::map m_filter_paths; + std::map m_known_paths; QList m_states; std::vector m_for_deletion; std::vector m_renders; diff --git a/retrace/daemon/ui/qml/StateControl.qml b/retrace/daemon/ui/qml/StateControl.qml index 2eda8a85..c0673b29 100644 --- a/retrace/daemon/ui/qml/StateControl.qml +++ b/retrace/daemon/ui/qml/StateControl.qml @@ -14,17 +14,52 @@ Item { anchors.fill: parent delegate: Component { Row { + visible: modelData.visible + height: modelData.visible ? combo.height : 0 + Rectangle { + id: indent + width: nameText.height * modelData.indent + height: 1 + opacity: 0.0 + } + Rectangle { + id: collapse + anchors.bottom: nameText.bottom + width: nameText.height + height: nameText.height + visible: modelData.choices.length == 0 + property var collapsed: false + color: collapse.collapsed ? "red" : "green" + MouseArea { + anchors.fill: parent + onClicked: { + if (collapse.collapsed) { + collapse.color = "green"; + stateModel.expand(modelData.path); + collapse.collapsed = false + } else { + collapse.color = "red"; + stateModel.collapse(modelData.path); + collapse.collapsed = true + } + } + } + } Text { id: nameText anchors.verticalCenter: parent.verticalCenter text: modelData.name + " : " } ComboBoxFitContents { + id: combo anchors.verticalCenter: parent.verticalCenter model: modelData.choices currentIndex: modelData.value + visible: (modelData.choices.length > 0) onActivated: { - stateModel.setState(modelData.name, + stateModel.setState(modelData.group, + modelData.path, + modelData.name, modelData.choices[currentIndex]); } } -- cgit v1.2.3