Compare commits
6 commits
d972f98ea4
...
3461198f9a
Author | SHA1 | Date | |
---|---|---|---|
3461198f9a | |||
9cb6327c98 | |||
cf8b9529c5 | |||
7e14e3f968 | |||
c19b428830 | |||
e8ca7e8a9e |
13 changed files with 375 additions and 179 deletions
|
@ -1,5 +1,7 @@
|
||||||
#include "base.h"
|
#include "base.h"
|
||||||
#include "meta.h"
|
#include "meta.h"
|
||||||
|
#include <ios>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
#define IMPL_WRAPPER_CLASS(CLASS_NAME, WRAPPED_TYPE, TYPE_NAME) Data::CLASS_NAME::CLASS_NAME(WRAPPED_TYPE in) : value(in) {} \
|
#define IMPL_WRAPPER_CLASS(CLASS_NAME, WRAPPED_TYPE, TYPE_NAME) Data::CLASS_NAME::CLASS_NAME(WRAPPED_TYPE in) : value(in) {} \
|
||||||
Data::CLASS_NAME::~CLASS_NAME() = default; \
|
Data::CLASS_NAME::~CLASS_NAME() = default; \
|
||||||
|
@ -45,7 +47,7 @@ Data::Variant Data::Bool::Deserialize(pugi::xml_node node) {
|
||||||
return Data::Bool(node.text().as_bool());
|
return Data::Bool(node.text().as_bool());
|
||||||
}
|
}
|
||||||
|
|
||||||
Data::Variant Data::Bool::FromString(std::string string) {
|
std::optional<Data::Variant> Data::Bool::FromString(std::string string) {
|
||||||
return Data::Bool(string[0] == 't' || string[0] == 'T' || string[0] == '1' || string[0] == 'y' || string[0] == 'Y');
|
return Data::Bool(string[0] == 't' || string[0] == 'T' || string[0] == '1' || string[0] == 'y' || string[0] == 'Y');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,21 +60,29 @@ Data::Variant Data::Int::Deserialize(pugi::xml_node node) {
|
||||||
return Data::Int(node.text().as_int());
|
return Data::Int(node.text().as_int());
|
||||||
}
|
}
|
||||||
|
|
||||||
Data::Variant Data::Int::FromString(std::string string) {
|
std::optional<Data::Variant> Data::Int::FromString(std::string string) {
|
||||||
return Data::Int(std::stoi(string));
|
char* endPos;
|
||||||
|
int value = (int)std::strtol(string.c_str(), &endPos, 10);
|
||||||
|
if (endPos == string.c_str()) return std::nullopt;
|
||||||
|
return Data::Int(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const Data::String Data::Float::ToString() const {
|
const Data::String Data::Float::ToString() const {
|
||||||
return Data::String(std::to_string(value));
|
std::stringstream stream;
|
||||||
|
stream << std::noshowpoint << value;
|
||||||
|
return Data::String(stream.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
Data::Variant Data::Float::Deserialize(pugi::xml_node node) {
|
Data::Variant Data::Float::Deserialize(pugi::xml_node node) {
|
||||||
return Data::Float(node.text().as_float());
|
return Data::Float(node.text().as_float());
|
||||||
}
|
}
|
||||||
|
|
||||||
Data::Variant Data::Float::FromString(std::string string) {
|
std::optional<Data::Variant> Data::Float::FromString(std::string string) {
|
||||||
return Data::Float(std::stof(string));
|
char* endPos;
|
||||||
|
float value = std::strtof(string.c_str(), &endPos);
|
||||||
|
if (endPos == string.c_str()) return std::nullopt;
|
||||||
|
return Data::Float(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,6 +94,6 @@ Data::Variant Data::String::Deserialize(pugi::xml_node node) {
|
||||||
return Data::String(node.text().as_string());
|
return Data::String(node.text().as_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
Data::Variant Data::String::FromString(std::string string) {
|
std::optional<Data::Variant> Data::String::FromString(std::string string) {
|
||||||
return Data::String(string);
|
return Data::String(string);
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
#include <pugixml.hpp>
|
#include <pugixml.hpp>
|
||||||
|
|
||||||
#define DEF_WRAPPER_CLASS(CLASS_NAME, WRAPPED_TYPE) class CLASS_NAME : public Data::Base { \
|
#define DEF_WRAPPER_CLASS(CLASS_NAME, WRAPPED_TYPE) class CLASS_NAME : public Data::Base { \
|
||||||
|
@ -16,13 +17,13 @@ public: \
|
||||||
virtual const Data::String ToString() const override; \
|
virtual const Data::String ToString() const override; \
|
||||||
virtual void Serialize(pugi::xml_node node) const override; \
|
virtual void Serialize(pugi::xml_node node) const override; \
|
||||||
static Data::Variant Deserialize(pugi::xml_node node); \
|
static Data::Variant Deserialize(pugi::xml_node node); \
|
||||||
static Data::Variant FromString(std::string); \
|
static std::optional<Data::Variant> FromString(std::string); \
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace Data {
|
namespace Data {
|
||||||
class Variant;
|
class Variant;
|
||||||
typedef std::function<Data::Variant(pugi::xml_node)> Deserializer;
|
typedef std::function<Data::Variant(pugi::xml_node)> Deserializer;
|
||||||
typedef std::function<Data::Variant(std::string)> FromString;
|
typedef std::function<std::optional<Data::Variant>(std::string)> FromString;
|
||||||
|
|
||||||
struct TypeInfo {
|
struct TypeInfo {
|
||||||
std::string name;
|
std::string name;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#include "vector.h"
|
#include "vector.h"
|
||||||
#include <glm/ext/quaternion_geometric.hpp>
|
#include <glm/ext/quaternion_geometric.hpp>
|
||||||
|
#include <string>
|
||||||
#include "meta.h" // IWYU pragma: keep
|
#include "meta.h" // IWYU pragma: keep
|
||||||
|
|
||||||
Data::Vector3::Vector3(const glm::vec3& src) : vector(src) {};
|
Data::Vector3::Vector3(const glm::vec3& src) : vector(src) {};
|
||||||
|
@ -10,6 +11,7 @@ Data::Vector3::~Vector3() = default;
|
||||||
const Data::TypeInfo Data::Vector3::TYPE = {
|
const Data::TypeInfo Data::Vector3::TYPE = {
|
||||||
.name = "Vector3",
|
.name = "Vector3",
|
||||||
.deserializer = &Data::Vector3::Deserialize,
|
.deserializer = &Data::Vector3::Deserialize,
|
||||||
|
.fromString = &Data::Vector3::FromString,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Data::TypeInfo& Data::Vector3::GetType() const { return Data::Vector3::TYPE; };
|
const Data::TypeInfo& Data::Vector3::GetType() const { return Data::Vector3::TYPE; };
|
||||||
|
@ -18,7 +20,10 @@ Data::Vector3 Data::Vector3::ZERO(0, 0, 0);
|
||||||
Data::Vector3 Data::Vector3::ONE(1, 1, 1);
|
Data::Vector3 Data::Vector3::ONE(1, 1, 1);
|
||||||
|
|
||||||
const Data::String Data::Vector3::ToString() const {
|
const Data::String Data::Vector3::ToString() const {
|
||||||
return std::to_string(X()) + ", " + std::to_string(Y()) + ", " + std::to_string(Z());
|
// https://stackoverflow.com/a/46424921/16255372
|
||||||
|
std::stringstream stream;
|
||||||
|
stream << std::setprecision(8) << std::noshowpoint << X() << ", " << Y() << ", " << Z();
|
||||||
|
return stream.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
Data::Vector3::operator glm::vec3() const { return vector; };
|
Data::Vector3::operator glm::vec3() const { return vector; };
|
||||||
|
@ -74,3 +79,24 @@ void Data::Vector3::Serialize(pugi::xml_node node) const {
|
||||||
Data::Variant Data::Vector3::Deserialize(pugi::xml_node node) {
|
Data::Variant Data::Vector3::Deserialize(pugi::xml_node node) {
|
||||||
return Data::Vector3(node.child("X").text().as_float(), node.child("Y").text().as_float(), node.child("Z").text().as_float());
|
return Data::Vector3(node.child("X").text().as_float(), node.child("Y").text().as_float(), node.child("Z").text().as_float());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<Data::Variant> Data::Vector3::FromString(std::string string) {
|
||||||
|
float components[3];
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
if (string.length() == 0) return std::nullopt;
|
||||||
|
while (string[0] == ' ' && string.length() > 0) string.erase(0, 1);
|
||||||
|
size_t nextPos = string.find(",");
|
||||||
|
if (nextPos == -1) nextPos = string.length();
|
||||||
|
std::string term = string.substr(0, nextPos);
|
||||||
|
string.erase(0, nextPos+1);
|
||||||
|
|
||||||
|
char* cpos;
|
||||||
|
float value = std::strtof(term.c_str(), &cpos);
|
||||||
|
if (cpos == term.c_str()) return std::nullopt;
|
||||||
|
|
||||||
|
components[i] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Data::Vector3(components[0], components[1], components[2]);
|
||||||
|
}
|
|
@ -25,7 +25,9 @@ namespace Data {
|
||||||
|
|
||||||
virtual const Data::String ToString() const override;
|
virtual const Data::String ToString() const override;
|
||||||
virtual void Serialize(pugi::xml_node node) const override;
|
virtual void Serialize(pugi::xml_node node) const override;
|
||||||
|
|
||||||
static Data::Variant Deserialize(pugi::xml_node node);
|
static Data::Variant Deserialize(pugi::xml_node node);
|
||||||
|
static std::optional<Data::Variant> FromString(std::string);
|
||||||
|
|
||||||
operator glm::vec3() const;
|
operator glm::vec3() const;
|
||||||
operator rp::Vector3() const;
|
operator rp::Vector3() const;
|
||||||
|
|
|
@ -25,13 +25,23 @@ const InstanceType Instance::TYPE = {
|
||||||
// return &TYPE_;
|
// return &TYPE_;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
constexpr FieldCodec classNameCodec() {
|
||||||
|
return FieldCodec {
|
||||||
|
.write = nullptr,
|
||||||
|
.read = [](void* source) -> Data::Variant {
|
||||||
|
return Data::String(((const InstanceType*)source)->className);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Instance::Instance(const InstanceType* type) {
|
Instance::Instance(const InstanceType* type) {
|
||||||
this->name = type->className;
|
this->name = type->className;
|
||||||
|
|
||||||
this->memberMap = std::make_unique<MemberMap>( MemberMap {
|
this->memberMap = std::make_unique<MemberMap>( MemberMap {
|
||||||
.super = std::nullopt,
|
.super = std::nullopt,
|
||||||
.members = {
|
.members = {
|
||||||
{ "Name", { .backingField = &name, .type = &Data::String::TYPE, .codec = fieldCodecOf<Data::String, std::string>() } }
|
{ "Name", { .backingField = &name, .type = &Data::String::TYPE, .codec = fieldCodecOf<Data::String, std::string>() } },
|
||||||
|
{ "ClassName", { .backingField = const_cast<InstanceType*>(type), .type = &Data::String::TYPE, .codec = classNameCodec(), .flags = PROP_READONLY } },
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -156,7 +166,7 @@ auto meta = GetPropertyMeta(name);
|
||||||
|
|
||||||
tl::expected<void, MemberNotFound> Instance::SetPropertyValue(std::string name, Data::Variant value) {
|
tl::expected<void, MemberNotFound> Instance::SetPropertyValue(std::string name, Data::Variant value) {
|
||||||
auto meta = GetPropertyMeta(name);
|
auto meta = GetPropertyMeta(name);
|
||||||
if (!meta) return tl::make_unexpected(MemberNotFound());
|
if (!meta || meta->flags & PROP_READONLY) return tl::make_unexpected(MemberNotFound());
|
||||||
|
|
||||||
meta->codec.write(value, meta->backingField);
|
meta->codec.write(value, meta->backingField);
|
||||||
if (meta->updateCallback) meta->updateCallback.value()(name);
|
if (meta->updateCallback) meta->updateCallback.value()(name);
|
||||||
|
|
|
@ -47,14 +47,27 @@ std::function<void(std::string name)> memberFunctionOf(void(T::*func)(std::strin
|
||||||
enum PropertyFlags {
|
enum PropertyFlags {
|
||||||
PROP_HIDDEN = 1 << 0, // Hidden from the editor
|
PROP_HIDDEN = 1 << 0, // Hidden from the editor
|
||||||
PROP_NOSAVE = 1 << 1, // Do not serialize
|
PROP_NOSAVE = 1 << 1, // Do not serialize
|
||||||
|
PROP_UNIT_FLOAT = 1 << 2, // Float between 0 and 1
|
||||||
|
PROP_READONLY = 1 << 3, // Read only property, do not write
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum PropertyCategory {
|
||||||
|
PROP_CATEGORY_APPEARENCE,
|
||||||
|
PROP_CATEGORY_DATA,
|
||||||
|
PROP_CATEGORY_BEHAVIOR,
|
||||||
|
PROP_CATEGORY_PART,
|
||||||
|
PROP_CATEGORY_SURFACE,
|
||||||
|
};
|
||||||
|
|
||||||
|
const int PROPERTY_CATEGORY_MAX = PROP_CATEGORY_SURFACE;
|
||||||
|
|
||||||
struct PropertyMeta {
|
struct PropertyMeta {
|
||||||
void* backingField;
|
void* backingField;
|
||||||
const Data::TypeInfo* type;
|
const Data::TypeInfo* type;
|
||||||
FieldCodec codec;
|
FieldCodec codec;
|
||||||
std::optional<std::function<void(std::string name)>> updateCallback;
|
std::optional<std::function<void(std::string name)>> updateCallback;
|
||||||
PropertyFlags flags;
|
PropertyFlags flags;
|
||||||
|
PropertyCategory category = PROP_CATEGORY_DATA;
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef std::variant<PropertyMeta> MemberMeta;
|
typedef std::variant<PropertyMeta> MemberMeta;
|
||||||
|
|
|
@ -30,7 +30,7 @@ constexpr FieldCodec cframePositionCodec() {
|
||||||
*cframe = cframe->Rotation() + source.get<Vector3>();
|
*cframe = cframe->Rotation() + source.get<Vector3>();
|
||||||
},
|
},
|
||||||
.read = [](void* source) -> Data::Variant {
|
.read = [](void* source) -> Data::Variant {
|
||||||
return *static_cast<Data::CFrame*>(source);
|
return static_cast<Data::CFrame*>(source)->Position();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -70,11 +70,13 @@ Part::Part(PartConstructParams params): Instance(&TYPE), cframe(Data::CFrame(par
|
||||||
.backingField = &anchored,
|
.backingField = &anchored,
|
||||||
.type = &Data::Bool::TYPE,
|
.type = &Data::Bool::TYPE,
|
||||||
.codec = fieldCodecOf<Data::Bool, bool>(),
|
.codec = fieldCodecOf<Data::Bool, bool>(),
|
||||||
.updateCallback = memberFunctionOf(&Part::onUpdated, this)
|
.updateCallback = memberFunctionOf(&Part::onUpdated, this),
|
||||||
|
.category = PROP_CATEGORY_BEHAVIOR,
|
||||||
}}, { "Locked", {
|
}}, { "Locked", {
|
||||||
.backingField = &locked,
|
.backingField = &locked,
|
||||||
.type = &Data::Bool::TYPE,
|
.type = &Data::Bool::TYPE,
|
||||||
.codec = fieldCodecOf<Data::Bool, bool>(),
|
.codec = fieldCodecOf<Data::Bool, bool>(),
|
||||||
|
.category = PROP_CATEGORY_BEHAVIOR,
|
||||||
}}, { "Position", {
|
}}, { "Position", {
|
||||||
.backingField = &cframe,
|
.backingField = &cframe,
|
||||||
.type = &Vector3::TYPE,
|
.type = &Vector3::TYPE,
|
||||||
|
@ -96,15 +98,19 @@ Part::Part(PartConstructParams params): Instance(&TYPE), cframe(Data::CFrame(par
|
||||||
.backingField = &size,
|
.backingField = &size,
|
||||||
.type = &Vector3::TYPE,
|
.type = &Vector3::TYPE,
|
||||||
.codec = fieldCodecOf<Vector3, glm::vec3>(),
|
.codec = fieldCodecOf<Vector3, glm::vec3>(),
|
||||||
.updateCallback = memberFunctionOf(&Part::onUpdated, this)
|
.updateCallback = memberFunctionOf(&Part::onUpdated, this),
|
||||||
|
.category = PROP_CATEGORY_PART,
|
||||||
}}, { "Color", {
|
}}, { "Color", {
|
||||||
.backingField = &color,
|
.backingField = &color,
|
||||||
.type = &Data::Color3::TYPE,
|
.type = &Data::Color3::TYPE,
|
||||||
.codec = fieldCodecOf<Data::Color3>(),
|
.codec = fieldCodecOf<Data::Color3>(),
|
||||||
|
.category = PROP_CATEGORY_APPEARENCE,
|
||||||
}}, { "Transparency", {
|
}}, { "Transparency", {
|
||||||
.backingField = &transparency,
|
.backingField = &transparency,
|
||||||
.type = &Data::Float::TYPE,
|
.type = &Data::Float::TYPE,
|
||||||
.codec = fieldCodecOf<Data::Float, float>(),
|
.codec = fieldCodecOf<Data::Float, float>(),
|
||||||
|
.flags = PROP_UNIT_FLOAT,
|
||||||
|
.category = PROP_CATEGORY_APPEARENCE,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -30,8 +30,6 @@ set(PROJECT_SOURCES
|
||||||
panes/explorermodel.cpp
|
panes/explorermodel.cpp
|
||||||
panes/propertiesview.h
|
panes/propertiesview.h
|
||||||
panes/propertiesview.cpp
|
panes/propertiesview.cpp
|
||||||
panes/propertiesmodel.h
|
|
||||||
panes/propertiesmodel.cpp
|
|
||||||
${TS_FILES}
|
${TS_FILES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -64,7 +62,7 @@ else()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_include_directories(editor PUBLIC "../core/src" "../include")
|
target_include_directories(editor PUBLIC "../core/src" "../include")
|
||||||
target_link_libraries(editor PRIVATE openblocks Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Multimedia)
|
target_link_libraries(editor PRIVATE openblocks Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::WidgetsPrivate Qt${QT_VERSION_MAJOR}::Multimedia)
|
||||||
|
|
||||||
# Qt6 does not include QOpenGLWidgets as part of Widgets base anymore, so
|
# Qt6 does not include QOpenGLWidgets as part of Widgets base anymore, so
|
||||||
# we have to include it manually
|
# we have to include it manually
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>935</width>
|
<width>1027</width>
|
||||||
<height>30</height>
|
<height>30</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
|
|
|
@ -1,118 +0,0 @@
|
||||||
#include "propertiesmodel.h"
|
|
||||||
#include "datatypes/base.h"
|
|
||||||
#include "datatypes/cframe.h"
|
|
||||||
#include "objects/base/member.h"
|
|
||||||
#include "qnamespace.h"
|
|
||||||
|
|
||||||
PropertiesModel::PropertiesModel(InstanceRef selectedItem, QWidget *parent)
|
|
||||||
: QAbstractItemModel(parent)
|
|
||||||
, selectedItem(selectedItem) {
|
|
||||||
this->propertiesList.reserve(selectedItem->GetProperties().size());
|
|
||||||
for (std::string name : selectedItem->GetProperties()) {
|
|
||||||
PropertyMeta meta = selectedItem->GetPropertyMeta(name).value();
|
|
||||||
// Don't show CFrames in properties
|
|
||||||
if (meta.type == &Data::CFrame::TYPE) continue;
|
|
||||||
|
|
||||||
this->propertiesList.push_back(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PropertiesModel::~PropertiesModel() = default;
|
|
||||||
|
|
||||||
|
|
||||||
QVariant PropertiesModel::data(const QModelIndex &index, int role) const {
|
|
||||||
if (!index.isValid())
|
|
||||||
return {};
|
|
||||||
|
|
||||||
std::string propertyName = propertiesList[index.row()];
|
|
||||||
PropertyMeta meta = selectedItem->GetPropertyMeta(propertyName).value();
|
|
||||||
|
|
||||||
switch (role) {
|
|
||||||
case Qt::EditRole:
|
|
||||||
case Qt::DisplayRole:
|
|
||||||
if (index.column() == 0)
|
|
||||||
return QString::fromStdString(propertyName);
|
|
||||||
else if (index.column() == 1 && meta.type != &Data::Bool::TYPE) {
|
|
||||||
return QString::fromStdString(selectedItem->GetPropertyValue(propertyName).value().ToString());
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
case Qt::CheckStateRole:
|
|
||||||
if (index.column() == 0) return {};
|
|
||||||
else if (index.column() == 1 && meta.type == &Data::Bool::TYPE)
|
|
||||||
return selectedItem->GetPropertyValue(propertyName)->get<Data::Bool>() ? Qt::Checked : Qt::Unchecked;
|
|
||||||
return {};
|
|
||||||
// case Qt::DecorationRole:
|
|
||||||
// return iconOf(item->GetClass());
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PropertiesModel::setData(const QModelIndex &index, const QVariant &value, int role) {
|
|
||||||
if (index.column() != 1) return false;
|
|
||||||
|
|
||||||
std::string propertyName = propertiesList[index.row()];
|
|
||||||
PropertyMeta meta = selectedItem->GetPropertyMeta(propertyName).value();
|
|
||||||
|
|
||||||
switch (role) {
|
|
||||||
case Qt::EditRole:
|
|
||||||
if (!meta.type->fromString)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
selectedItem->SetPropertyValue(propertyName, meta.type->fromString(value.toString().toStdString()));
|
|
||||||
return true;
|
|
||||||
case Qt::CheckStateRole:
|
|
||||||
if (meta.type != &Data::Bool::TYPE)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
selectedItem->SetPropertyValue(propertyName, Data::Bool(value.toBool()));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Qt::ItemFlags PropertiesModel::flags(const QModelIndex &index) const {
|
|
||||||
if (!index.isValid())
|
|
||||||
return Qt::NoItemFlags;
|
|
||||||
|
|
||||||
if (index.column() == 0)
|
|
||||||
return Qt::ItemIsEnabled;
|
|
||||||
|
|
||||||
std::string propertyName = propertiesList[index.row()];
|
|
||||||
PropertyMeta meta = selectedItem->GetPropertyMeta(propertyName).value();
|
|
||||||
|
|
||||||
if (index.column() == 1) {
|
|
||||||
if (meta.type == &Data::Bool::TYPE)
|
|
||||||
return Qt::ItemIsEnabled | Qt::ItemIsUserCheckable;
|
|
||||||
else
|
|
||||||
return Qt::ItemIsEnabled | Qt::ItemIsEditable;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Qt::NoItemFlags;
|
|
||||||
};
|
|
||||||
|
|
||||||
QVariant PropertiesModel::headerData(int section, Qt::Orientation orientation,
|
|
||||||
int role) const {
|
|
||||||
return QString("");
|
|
||||||
}
|
|
||||||
|
|
||||||
QModelIndex PropertiesModel::index(int row, int column,
|
|
||||||
const QModelIndex &parent) const {
|
|
||||||
if (!hasIndex(row, column, parent))
|
|
||||||
return {};
|
|
||||||
|
|
||||||
return createIndex(row, column);
|
|
||||||
}
|
|
||||||
|
|
||||||
QModelIndex PropertiesModel::parent(const QModelIndex &index) const {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
int PropertiesModel::rowCount(const QModelIndex &parent) const {
|
|
||||||
return !parent.isValid() ? this->propertiesList.size() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int PropertiesModel::columnCount(const QModelIndex &parent) const {
|
|
||||||
return 2;
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "objects/base/instance.h"
|
|
||||||
#include "qabstractitemmodel.h"
|
|
||||||
#include "qnamespace.h"
|
|
||||||
#include <QOpenGLWidget>
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
class PropertiesModel : public QAbstractItemModel {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
Q_DISABLE_COPY_MOVE(PropertiesModel)
|
|
||||||
|
|
||||||
explicit PropertiesModel(InstanceRef selectedItem, QWidget *parent = nullptr);
|
|
||||||
~PropertiesModel() override;
|
|
||||||
|
|
||||||
QVariant data(const QModelIndex &index, int role) const override;
|
|
||||||
bool setData(const QModelIndex &index, const QVariant &value, int role) 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 = {}) const override;
|
|
||||||
QModelIndex parent(const QModelIndex &index) const override;
|
|
||||||
int rowCount(const QModelIndex &parent = {}) const override;
|
|
||||||
int columnCount(const QModelIndex &parent = {}) const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
InstanceRef selectedItem;
|
|
||||||
std::vector<std::string> propertiesList;
|
|
||||||
};
|
|
|
@ -1,20 +1,287 @@
|
||||||
#include "propertiesview.h"
|
#include "propertiesview.h"
|
||||||
#include "propertiesmodel.h"
|
#include "datatypes/base.h"
|
||||||
#include "qaction.h"
|
#include "datatypes/meta.h"
|
||||||
|
#include "objects/base/member.h"
|
||||||
|
#include <map>
|
||||||
|
#include <qabstractitemdelegate.h>
|
||||||
|
#include <qbrush.h>
|
||||||
|
#include <qlineedit.h>
|
||||||
|
#include <qnamespace.h>
|
||||||
|
#include <qpalette.h>
|
||||||
|
#include <qspinbox.h>
|
||||||
|
#include <qstyle.h>
|
||||||
|
#include <qstyleditemdelegate.h>
|
||||||
|
#include <qstyleoption.h>
|
||||||
|
#include <qtreewidget.h>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QStyledItemDelegate>
|
||||||
|
#include <private/qtreeview_p.h>
|
||||||
|
#include <QDoubleSpinBox>
|
||||||
|
|
||||||
|
class CustomItemDelegate : public QStyledItemDelegate {
|
||||||
|
PropertiesView* view;
|
||||||
|
public:
|
||||||
|
CustomItemDelegate(PropertiesView* parent) : view(parent), QStyledItemDelegate(parent) {}
|
||||||
|
|
||||||
|
void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override {
|
||||||
|
// https://stackoverflow.com/a/76645757/16255372
|
||||||
|
// https://stackoverflow.com/a/70078448/16255372
|
||||||
|
|
||||||
|
int indent = dynamic_cast<PropertiesView*>(parent())->indentation();
|
||||||
|
|
||||||
|
QStyledItemDelegate::initStyleOption(option, index);
|
||||||
|
|
||||||
|
if (index.parent().isValid()) {
|
||||||
|
option->rect.adjust(-indent, 0, -indent, 0);
|
||||||
|
} else {
|
||||||
|
option->state &= ~QStyle::State_Selected;
|
||||||
|
|
||||||
|
option->backgroundBrush = view->palette().dark();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override {
|
||||||
|
if (index.column() == 0) return nullptr;
|
||||||
|
|
||||||
|
if (!index.parent().isValid() || !view->currentInstance || view->currentInstance->expired()) return nullptr;
|
||||||
|
InstanceRef inst = view->currentInstance->lock();
|
||||||
|
|
||||||
|
std::string propertyName = view->itemFromIndex(index)->data(0, Qt::DisplayRole).toString().toStdString();
|
||||||
|
PropertyMeta meta = inst->GetPropertyMeta(propertyName).value();
|
||||||
|
Data::Variant currentValue = inst->GetPropertyValue(propertyName).value();
|
||||||
|
|
||||||
|
if (meta.type == &Data::Float::TYPE) {
|
||||||
|
QDoubleSpinBox* spinBox = new QDoubleSpinBox(parent);
|
||||||
|
spinBox->setValue(currentValue.get<Data::Float>());
|
||||||
|
|
||||||
|
if (meta.flags & PROP_UNIT_FLOAT) {
|
||||||
|
spinBox->setMinimum(0);
|
||||||
|
spinBox->setMaximum(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return spinBox;
|
||||||
|
} else if (meta.type == &Data::Int::TYPE) {
|
||||||
|
QSpinBox* spinBox = new QSpinBox(parent);
|
||||||
|
spinBox->setValue(currentValue.get<Data::Int>());
|
||||||
|
|
||||||
|
return spinBox;
|
||||||
|
} else if (meta.type == &Data::String::TYPE) {
|
||||||
|
QLineEdit* lineEdit = new QLineEdit(parent);
|
||||||
|
lineEdit->setText(QString::fromStdString(currentValue.get<Data::String>()));
|
||||||
|
|
||||||
|
return lineEdit;
|
||||||
|
} else if (meta.type->fromString) {
|
||||||
|
QLineEdit* lineEdit = new QLineEdit(parent);
|
||||||
|
lineEdit->setText(QString::fromStdString(currentValue.ToString()));
|
||||||
|
|
||||||
|
return lineEdit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &/* index */) const override {
|
||||||
|
editor->setGeometry(option.rect.adjusted(-view->indentation(), 0, -view->indentation(), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setEditorData(QWidget *editor, const QModelIndex &index) const override {
|
||||||
|
if (index.column() == 0) return;
|
||||||
|
|
||||||
|
if (!index.parent().isValid() || !view->currentInstance || view->currentInstance->expired()) return;
|
||||||
|
InstanceRef inst = view->currentInstance->lock();
|
||||||
|
|
||||||
|
std::string propertyName = view->itemFromIndex(index)->data(0, Qt::DisplayRole).toString().toStdString();
|
||||||
|
PropertyMeta meta = inst->GetPropertyMeta(propertyName).value();
|
||||||
|
Data::Variant currentValue = inst->GetPropertyValue(propertyName).value();
|
||||||
|
|
||||||
|
if (meta.type == &Data::Float::TYPE) {
|
||||||
|
QDoubleSpinBox* spinBox = dynamic_cast<QDoubleSpinBox*>(editor);
|
||||||
|
|
||||||
|
spinBox->setValue(currentValue.get<Data::Float>());
|
||||||
|
} else if (meta.type == &Data::Int::TYPE) {
|
||||||
|
QSpinBox* spinBox = dynamic_cast<QSpinBox*>(editor);
|
||||||
|
|
||||||
|
spinBox->setValue(currentValue.get<Data::Int>());
|
||||||
|
} else if (meta.type == &Data::String::TYPE) {
|
||||||
|
QLineEdit* lineEdit = dynamic_cast<QLineEdit*>(editor);
|
||||||
|
|
||||||
|
lineEdit->setText(QString::fromStdString((std::string)currentValue.get<Data::String>()));
|
||||||
|
} else if (meta.type->fromString) {
|
||||||
|
QLineEdit* lineEdit = dynamic_cast<QLineEdit*>(editor);
|
||||||
|
|
||||||
|
lineEdit->setText(QString::fromStdString((std::string)currentValue.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override {
|
||||||
|
if (index.column() == 0) return;
|
||||||
|
|
||||||
|
if (!index.parent().isValid() || !view->currentInstance || view->currentInstance->expired()) return;
|
||||||
|
InstanceRef inst = view->currentInstance->lock();
|
||||||
|
|
||||||
|
std::string propertyName = view->itemFromIndex(index)->data(0, Qt::DisplayRole).toString().toStdString();
|
||||||
|
PropertyMeta meta = inst->GetPropertyMeta(propertyName).value();
|
||||||
|
|
||||||
|
if (meta.type == &Data::Float::TYPE) {
|
||||||
|
QDoubleSpinBox* spinBox = dynamic_cast<QDoubleSpinBox*>(editor);
|
||||||
|
|
||||||
|
inst->SetPropertyValue(propertyName, Data::Float((float)spinBox->value()));
|
||||||
|
model->setData(index, spinBox->value());
|
||||||
|
} else if (meta.type == &Data::Int::TYPE) {
|
||||||
|
QSpinBox* spinBox = dynamic_cast<QSpinBox*>(editor);
|
||||||
|
|
||||||
|
inst->SetPropertyValue(propertyName, Data::Int((float)spinBox->value()));
|
||||||
|
model->setData(index, spinBox->value());
|
||||||
|
} else if (meta.type == &Data::String::TYPE) {
|
||||||
|
QLineEdit* lineEdit = dynamic_cast<QLineEdit*>(editor);
|
||||||
|
|
||||||
|
inst->SetPropertyValue(propertyName, Data::String(lineEdit->text().toStdString()));
|
||||||
|
model->setData(index, lineEdit->text());
|
||||||
|
} else if (meta.type->fromString) {
|
||||||
|
QLineEdit* lineEdit = dynamic_cast<QLineEdit*>(editor);
|
||||||
|
|
||||||
|
std::optional<Data::Variant> parsedResult = meta.type->fromString(lineEdit->text().toStdString());
|
||||||
|
if (!parsedResult) return;
|
||||||
|
inst->SetPropertyValue(propertyName, parsedResult.value());
|
||||||
|
model->setData(index, QString::fromStdString(parsedResult.value().ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
PropertiesView::PropertiesView(QWidget* parent):
|
PropertiesView::PropertiesView(QWidget* parent):
|
||||||
QTreeView(parent) {
|
QTreeWidget(parent) {
|
||||||
this->setStyleSheet(QString("QTreeView::branch { border: none; }"));
|
|
||||||
|
clear();
|
||||||
|
setHeaderHidden(true);
|
||||||
|
setColumnCount(2);
|
||||||
|
setAlternatingRowColors(true);
|
||||||
|
setItemDelegate(new CustomItemDelegate(this));
|
||||||
|
|
||||||
|
connect(this, &QTreeWidget::itemChanged, this, &PropertiesView::propertyChanged);
|
||||||
|
connect(this, &QTreeWidget::itemActivated, this, [&](auto* item, int column) {
|
||||||
|
// Prevent editing the first column
|
||||||
|
if (column == 0)
|
||||||
|
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
|
||||||
|
else if (item->parent())
|
||||||
|
item->setFlags(item->flags() | Qt::ItemIsEditable);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
PropertiesView::~PropertiesView() {
|
PropertiesView::~PropertiesView() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QStringList PROPERTY_CATEGORY_NAMES {
|
||||||
|
"Appearence",
|
||||||
|
"Data",
|
||||||
|
"Behavior",
|
||||||
|
"Part",
|
||||||
|
"Surface"
|
||||||
|
};
|
||||||
|
|
||||||
|
QModelIndex PropertiesView::indexAt(const QPoint &point) const {
|
||||||
|
return QTreeWidget::indexAt(point + QPoint(indentation(), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PropertiesView::drawBranches(QPainter *painter, const QRect &rect, const QModelIndex &index) const {
|
||||||
|
// https://codebrowser.dev/qt5/qtbase/src/widgets/itemviews/qtreeview.cpp.html#312opt
|
||||||
|
Q_D(const QTreeView);
|
||||||
|
QStyleOptionViewItem opt = viewOptions();
|
||||||
|
|
||||||
|
const QTreeViewItem& viewItem = d->viewItems.at(d->current);
|
||||||
|
|
||||||
|
// Taken from source code (above)
|
||||||
|
bool hoverRow = selectionBehavior() == QAbstractItemView::SelectRows
|
||||||
|
&& opt.showDecorationSelected
|
||||||
|
&& index.parent() == d->hover.parent()
|
||||||
|
&& index.row() == d->hover.row();
|
||||||
|
|
||||||
|
// Un-indent branch
|
||||||
|
opt.rect = rect;
|
||||||
|
if (index.parent().isValid())
|
||||||
|
opt.rect.adjust(0, 0, -indentation(), 0);
|
||||||
|
opt.state |= QStyle::State_Item;
|
||||||
|
if (viewItem.hasChildren)
|
||||||
|
opt.state |= QStyle::State_Children;
|
||||||
|
if (viewItem.expanded)
|
||||||
|
opt.state |= QStyle::State_Open;
|
||||||
|
if (viewItem.hasMoreSiblings || viewItem.parentItem > -1 && d->viewItems.at(viewItem.parentItem).hasMoreSiblings)
|
||||||
|
opt.state |= QStyle::State_Sibling;
|
||||||
|
|
||||||
|
opt.state.setFlag(QStyle::State_MouseOver, hoverRow || d->current == d->hoverBranch);
|
||||||
|
|
||||||
|
// Draw background for headings
|
||||||
|
if (!index.parent().isValid())
|
||||||
|
painter->fillRect(opt.rect, palette().dark());
|
||||||
|
|
||||||
|
style()->drawPrimitive(QStyle::PE_IndicatorBranch, &opt, painter, this);
|
||||||
|
}
|
||||||
|
|
||||||
void PropertiesView::setSelected(std::optional<InstanceRef> instance) {
|
void PropertiesView::setSelected(std::optional<InstanceRef> instance) {
|
||||||
if (instance.has_value()) {
|
clear();
|
||||||
this->setModel(new PropertiesModel(instance.value()));
|
if (!instance) return;
|
||||||
} else {
|
InstanceRef inst = instance.value();
|
||||||
if (this->model()) delete this->model();
|
currentInstance = inst;
|
||||||
this->setModel(nullptr);
|
|
||||||
|
std::map<PropertyCategory, QTreeWidgetItem*> propertyCategories;
|
||||||
|
|
||||||
|
for (int i = 0; i <= PROPERTY_CATEGORY_MAX; i++) {
|
||||||
|
QTreeWidgetItem* item = new QTreeWidgetItem;
|
||||||
|
|
||||||
|
QBrush brush;
|
||||||
|
brush.setColor(QPalette::Midlight);
|
||||||
|
brush.setStyle(Qt::SolidPattern);
|
||||||
|
|
||||||
|
item->setData(0, Qt::DisplayRole, PROPERTY_CATEGORY_NAMES[i]);
|
||||||
|
item->setFirstColumnSpanned(true);
|
||||||
|
|
||||||
|
propertyCategories[(PropertyCategory)i] = item;
|
||||||
|
addTopLevelItem(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> properties = inst->GetProperties();
|
||||||
|
|
||||||
|
for (std::string property : properties) {
|
||||||
|
PropertyMeta meta = inst->GetPropertyMeta(property).value();
|
||||||
|
Data::Variant currentValue = inst->GetPropertyValue(property).value();
|
||||||
|
|
||||||
|
if (meta.type == &Data::CFrame::TYPE) continue;
|
||||||
|
|
||||||
|
QTreeWidgetItem* item = new QTreeWidgetItem;
|
||||||
|
item->setFlags(item->flags() | Qt::ItemIsEditable | Qt::ItemIsSelectable);
|
||||||
|
item->setData(0, Qt::DisplayRole, QString::fromStdString(property));
|
||||||
|
|
||||||
|
if (meta.type == &Data::Bool::TYPE) {
|
||||||
|
item->setCheckState(1, (bool)currentValue.get<Data::Bool>() ? Qt::CheckState::Checked : Qt::CheckState::Unchecked);
|
||||||
|
} else {
|
||||||
|
item->setData(1, Qt::DisplayRole, QString::fromStdString(currentValue.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!meta.type->fromString || meta.flags & PROP_READONLY) {
|
||||||
|
item->setDisabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
propertyCategories[meta.category]->addChild(item);
|
||||||
|
propertyCategories[meta.category]->setExpanded(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove child-less categories
|
||||||
|
for (int i = 0; i <= PROPERTY_CATEGORY_MAX; i++) {
|
||||||
|
if (propertyCategories[(PropertyCategory)i]->childCount() > 0) continue;
|
||||||
|
int idx = indexOfTopLevelItem(propertyCategories[(PropertyCategory)i]);
|
||||||
|
delete takeTopLevelItem(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeColumnToContents(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PropertiesView::propertyChanged(QTreeWidgetItem *item, int column) {
|
||||||
|
if (!item->parent() || !currentInstance || currentInstance->expired()) return;
|
||||||
|
InstanceRef inst = currentInstance->lock();
|
||||||
|
|
||||||
|
std::string propertyName = item->data(0, Qt::DisplayRole).toString().toStdString();
|
||||||
|
PropertyMeta meta = inst->GetPropertyMeta(propertyName).value();
|
||||||
|
|
||||||
|
if (meta.type == &Data::Bool::TYPE) {
|
||||||
|
inst->SetPropertyValue(propertyName, Data::Bool(item->checkState(1)));
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,23 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <QTreeWidget>
|
||||||
#include "objects/base/instance.h"
|
#include "objects/base/instance.h"
|
||||||
#include "qtreeview.h"
|
|
||||||
|
|
||||||
class Ui_MainWindow;
|
class Ui_MainWindow;
|
||||||
|
|
||||||
class PropertiesView : public QTreeView {
|
class CustomItemDelegate;
|
||||||
|
|
||||||
|
class PropertiesView : public QTreeWidget {
|
||||||
|
Q_DECLARE_PRIVATE(QTreeView)
|
||||||
|
|
||||||
|
std::optional<InstanceRefWeak> currentInstance;
|
||||||
|
void propertyChanged(QTreeWidgetItem *item, int column);
|
||||||
|
void activateProperty(QTreeWidgetItem *item, int column);
|
||||||
|
|
||||||
|
friend CustomItemDelegate;
|
||||||
|
protected:
|
||||||
|
void drawBranches(QPainter *painter, const QRect &rect, const QModelIndex &index) const override;
|
||||||
|
QModelIndex indexAt(const QPoint &point) const override;
|
||||||
public:
|
public:
|
||||||
PropertiesView(QWidget* parent = nullptr);
|
PropertiesView(QWidget* parent = nullptr);
|
||||||
~PropertiesView() override;
|
~PropertiesView() override;
|
||||||
|
|
Loading…
Add table
Reference in a new issue