/* * OpenClonk, http://www.openclonk.org * * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/ * Copyright (c) 2013, The OpenClonk Team and contributors * * Distributed under the terms of the ISC license; see accompanying file * "COPYING" for details. * * "Clonk" is a registered trademark of Matthes Bender, used with permission. * See accompanying file "TRADEMARK" for details. * * To redistribute this file separately, substitute the full license texts * for the above references. */ #include "C4Include.h" #include "script/C4Value.h" #include "editor/C4ConsoleQtPropListViewer.h" #include "editor/C4ConsoleQtState.h" #include "editor/C4Console.h" #include "object/C4Object.h" #include "object/C4DefList.h" #include "object/C4Def.h" #include "script/C4Effect.h" #include "script/C4AulExec.h" /* Property path for property setting synchronization */ C4PropertyPath::C4PropertyPath(C4PropList *target) { // Build string to set target: supports objects only if (target) { C4Object *obj = target->GetObject(); if (obj) path.Format("Object(%d)", (int)obj->Number); } } C4PropertyPath::C4PropertyPath(C4Effect *fx, C4Object *target_obj) { // Effect property path: Represent as GetEffect("name", Object(%d), index) if (!fx || !target_obj) return; const char *name = fx->GetName(); int32_t index = 0; for (C4Effect *ofx = target_obj->pEffects; ofx; ofx = ofx->pNext) if (ofx == fx) break; else if (!strcmp(ofx->GetName(), name)) ++index; path.Format("GetEffect(\"%s\", Object(%d), %d)", name, (int)target_obj->Number, (int)index); } C4PropertyPath::C4PropertyPath(const C4PropertyPath &parent, int32_t elem_index) { path.Format("%s[%d]", parent.GetPath(), (int)elem_index); path_type = PPT_Index; } C4PropertyPath::C4PropertyPath(const C4PropertyPath &parent, const char *child_property, C4PropertyPath::PathType path_type) : path_type(path_type) { if (path_type == PPT_Property) path.Format("%s.%s", parent.GetPath(), child_property); else if (path_type == PPT_SetFunction) path.Format("%s->%s", parent.GetPath(), child_property); else if (path_type == PPT_GlobalSetFunction) { path.Copy(parent.GetPath()); argument.Copy(child_property); } else { assert(false); } } void C4PropertyPath::SetProperty(const char *set_string) const { // Compose script to update property StdStrBuf script; if (path_type == PPT_SetFunction) script.Format("%s(%s)", path.getData(), set_string); else if (path_type == PPT_GlobalSetFunction) script.Format("%s(%s,%s)", argument.getData(), path.getData(), set_string); else script.Format("%s=%s", path.getData(), set_string); // Execute synced scripted ::Console.EditCursor.EMControl(CID_Script, new C4ControlScript(script.getData(), 0, false)); } void C4PropertyPath::SetProperty(const C4Value &to_val) const { SetProperty(to_val.GetDataString(9999999).getData()); } C4Value C4PropertyPath::ResolveValue() const { return AulExec.DirectExec(::ScriptEngine.GetPropList(), path.getData(), "resolve property", false, nullptr); } /* Property editing */ C4PropertyDelegate::C4PropertyDelegate(const C4PropertyDelegateFactory *factory, C4PropList *props) : QObject(), factory(factory), set_function_is_global(false) { // Resolve getter+setter callback names if (props) { set_function = props->GetPropertyStr(P_Set); set_function_is_global = props->GetPropertyBool(P_Global); async_get_function = props->GetPropertyStr(P_AsyncGet); } } void C4PropertyDelegate::UpdateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option) const { editor->setGeometry(option.rect); } bool C4PropertyDelegate::GetPropertyValue(C4PropList *props, C4String *key, C4Value *out_val) const { if (async_get_function) { *out_val = props->Call(async_get_function.Get()); return true; } else { return props->GetPropertyByS(key, out_val); } } QString C4PropertyDelegate::GetDisplayString(const C4Value &v, C4Object *obj) const { return QString(v.GetDataString().getData()); } QColor C4PropertyDelegate::GetDisplayTextColor(const C4Value &val, class C4Object *obj) const { return QColor(); // invalid = default } QColor C4PropertyDelegate::GetDisplayBackgroundColor(const C4Value &val, class C4Object *obj) const { return QColor(); // invalid = default } C4PropertyPath C4PropertyDelegate::GetPathForProperty(C4ConsoleQtPropListModelProperty *editor_prop) const { C4PropertyPath path; if (editor_prop->property_path.IsEmpty()) path = C4PropertyPath(editor_prop->parent_proplist.getPropList()); else path = editor_prop->property_path; C4PropertyPath subpath; if (GetSetFunction()) subpath = C4PropertyPath(path, GetSetFunction(), IsGlobalSetFunction() ? C4PropertyPath::PPT_GlobalSetFunction : C4PropertyPath::PPT_SetFunction); else subpath = C4PropertyPath(path, editor_prop->key->GetCStr()); return subpath; } C4PropertyDelegateInt::C4PropertyDelegateInt(const C4PropertyDelegateFactory *factory, C4PropList *props) : C4PropertyDelegate(factory, props), min(std::numeric_limits::min()), max(std::numeric_limits::max()), step(1) { // TODO min/max/step if (props) { min = props->GetPropertyInt(P_Min, min); max = props->GetPropertyInt(P_Max, max); step = props->GetPropertyInt(P_Step, step); } } void C4PropertyDelegateInt::SetEditorData(QWidget *editor, const C4Value &val, const C4PropertyPath &property_path) const { QSpinBox *spinBox = static_cast(editor); spinBox->setValue(val.getInt()); } void C4PropertyDelegateInt::SetModelData(QObject *editor, const C4PropertyPath &property_path) const { QSpinBox *spinBox = static_cast(editor); spinBox->interpretText(); property_path.SetProperty(C4VInt(spinBox->value())); } QWidget *C4PropertyDelegateInt::CreateEditor(const C4PropertyDelegateFactory *parent_delegate, QWidget *parent, const QStyleOptionViewItem &option) const { QSpinBox *editor = new QSpinBox(parent); editor->setMinimum(min); editor->setMaximum(max); editor->setSingleStep(step); connect(editor, &QSpinBox::editingFinished, this, [editor, this]() { emit EditingDoneSignal(editor); }); return editor; } C4PropertyDelegateLabelAndButtonWidget::C4PropertyDelegateLabelAndButtonWidget(QWidget *parent) : QWidget(parent), layout(nullptr), label(nullptr), button(nullptr) { layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setMargin(0); layout->setSpacing(0); label = new QLabel(this); layout->addWidget(label); button = new QPushButton(QString(LoadResStr("IDS_CNS_MORE")), this); layout->addWidget(button); } C4PropertyDelegateColor::C4PropertyDelegateColor(const class C4PropertyDelegateFactory *factory, C4PropList *props) : C4PropertyDelegate(factory, props) { } uint32_t GetTextColorForBackground(uint32_t background_color) { // White text on dark background; black text on bright background uint8_t r = (background_color >> 16) & 0xff; uint8_t g = (background_color >> 8) & 0xff; uint8_t b = (background_color >> 0) & 0xff; int32_t lgt = r * 30 + g * 59 + b * 11; return (lgt > 16000) ? 0 : 0xffffff; } void C4PropertyDelegateColor::SetEditorData(QWidget *aeditor, const C4Value &val, const C4PropertyPath &property_path) const { Editor *editor = static_cast(aeditor); uint32_t background_color = static_cast(val.getInt()) & 0xffffff; uint32_t foreground_color = GetTextColorForBackground(background_color); QPalette palette = editor->label->palette(); palette.setColor(editor->label->backgroundRole(), QColor(QRgb(background_color))); palette.setColor(editor->label->foregroundRole(), QColor(QRgb(foreground_color))); editor->label->setPalette(palette); editor->label->setAutoFillBackground(true); editor->label->setText(GetDisplayString(val, NULL)); editor->last_value = val; } void C4PropertyDelegateColor::SetModelData(QObject *aeditor, const C4PropertyPath &property_path) const { Editor *editor = static_cast(aeditor); property_path.SetProperty(editor->last_value); } QWidget *C4PropertyDelegateColor::CreateEditor(const class C4PropertyDelegateFactory *parent_delegate, QWidget *parent, const QStyleOptionViewItem &option) const { Editor *editor; std::unique_ptr peditor((editor = new Editor(parent))); connect(editor->button, &QPushButton::pressed, this, [editor, this]() { QColor clr = QColorDialog::getColor(QColor(editor->last_value.getInt()), editor, QString(), QColorDialog::ShowAlphaChannel); editor->last_value.SetInt(clr.rgba()); this->SetEditorData(editor, editor->last_value, C4PropertyPath()); // force update on display emit EditingDoneSignal(editor); }); return peditor.release(); } QString C4PropertyDelegateColor::GetDisplayString(const C4Value &v, C4Object *obj) const { return QString("#%1").arg(uint32_t(v.getInt()), 8, 16, QChar('0')); } QColor C4PropertyDelegateColor::GetDisplayTextColor(const C4Value &val, class C4Object *obj) const { uint32_t background_color = static_cast(val.getInt()) & 0xffffff; uint32_t foreground_color = GetTextColorForBackground(background_color); return QColor(foreground_color); } QColor C4PropertyDelegateColor::GetDisplayBackgroundColor(const C4Value &val, class C4Object *obj) const { return static_cast(val.getInt()) & 0xffffff; } C4PropertyDelegateEnum::C4PropertyDelegateEnum(const C4PropertyDelegateFactory *factory, C4PropList *props, const C4ValueArray *poptions) : C4PropertyDelegate(factory, props) { // Build enum options from C4Value definitions in script if (!poptions && props) poptions = props->GetPropertyArray(P_Options); if (poptions) { options.reserve(poptions->GetSize()); for (int32_t i = 0; i < poptions->GetSize(); ++i) { const C4Value &v = poptions->GetItem(i); C4PropList *props = v.getPropList(); if (!props) continue; Option option; option.name = props->GetPropertyStr(P_Name); if (!option.name.Get()) option.name = ::Strings.RegString("???"); option.value_key = props->GetPropertyStr(P_ValueKey); props->GetProperty(P_Value, &option.value); option.type = C4V_Type(props->GetPropertyInt(P_Type, C4V_Any)); option.option_key = props->GetPropertyStr(P_OptionKey); // Derive storage type from given elements in delegate definition if (option.type != C4V_Any) option.storage_type = Option::StorageByType; else if (option.option_key.Get()) option.storage_type = Option::StorageByKey; else option.storage_type = Option::StorageByValue; // Child delegate for value (resolved at runtime because there may be circular references) props->GetProperty(P_Delegate, &option.adelegate_val); options.push_back(option); } } } void C4PropertyDelegateEnum::ReserveOptions(int32_t num) { options.reserve(num); } void C4PropertyDelegateEnum::AddTypeOption(C4String *name, C4V_Type type, const C4Value &val, C4PropertyDelegate *adelegate) { Option option; option.name = name; option.type = type; option.value = val; option.storage_type = Option::StorageByType; option.adelegate = adelegate; options.push_back(option); } void C4PropertyDelegateEnum::AddConstOption(C4String *name, const C4Value &val) { Option option; option.name = name; option.value = val; option.storage_type = Option::StorageByValue; options.push_back(option); } int32_t C4PropertyDelegateEnum::GetOptionByValue(const C4Value &val) const { int32_t iopt = 0; bool match = false; for (auto &option : options) { switch (option.storage_type) { case Option::StorageByType: match = (val.GetTypeEx() == option.type); break; case Option::StorageByValue: match = (val == option.value); break; case Option::StorageByKey: // Compare value to value in property. Assume undefined as nil. { C4PropList *props = val.getPropList(); if (props) { C4Value propval; props->GetPropertyByS(option.option_key.Get(), &propval); match = (val == propval); } break; } default: break; } if (match) break; ++iopt; } // If no option matches, just pick first return match ? iopt : -1; } void C4PropertyDelegateEnum::UpdateEditorParameter(C4PropertyDelegateEnum::Editor *editor) const { // Recreate parameter settings editor associated with the currently selected option of an enum if (editor->parameter_widget) { editor->parameter_widget->deleteLater(); editor->parameter_widget = NULL; } int32_t idx = editor->option_box->currentIndex(); if (idx < 0 || idx >= options.size()) return; const Option &option = options[idx]; // Lazy-resolve parameter delegate EnsureOptionDelegateResolved(option); // Create editor if needed if (option.adelegate) { // Determine value to be shown in editor C4Value parameter_val = editor->last_val; if (option.value_key.Get()) { C4PropList *props = editor->last_val.getPropList(); if (props) props->GetPropertyByS(option.value_key.Get(), ¶meter_val); } // Show it editor->parameter_widget = option.adelegate->CreateEditor(factory, editor, QStyleOptionViewItem()); if (editor->parameter_widget) { editor->layout->addWidget(editor->parameter_widget); option.adelegate->SetEditorData(editor->parameter_widget, parameter_val, editor->last_get_path); // Forward editing signals connect(option.adelegate, &C4PropertyDelegate::EditorValueChangedSignal, editor->parameter_widget, [this, editor](QWidget *changed_editor) { if (changed_editor == editor->parameter_widget) if (!editor->updating) emit EditorValueChangedSignal(editor); }); connect(option.adelegate, &C4PropertyDelegate::EditingDoneSignal, editor->parameter_widget, [this, editor](QWidget *changed_editor) { if (changed_editor == editor->parameter_widget) emit EditingDoneSignal(editor); }); } } } void C4PropertyDelegateEnum::SetEditorData(QWidget *aeditor, const C4Value &val, const C4PropertyPath &property_path) const { Editor *editor = static_cast(aeditor); editor->last_val = val; editor->last_get_path = property_path; editor->updating = true; // Update option selection int32_t index = std::max(GetOptionByValue(val), 0); editor->option_box->setCurrentIndex(index); // Update parameter UpdateEditorParameter(editor); editor->updating = false; } void C4PropertyDelegateEnum::SetModelData(QObject *aeditor, const C4PropertyPath &property_path) const { // Fetch value from editor Editor *editor = static_cast(aeditor); int32_t idx = editor->option_box->currentIndex(); if (idx < 0 || idx >= options.size()) return; const Option &option = options[idx]; // Store directly in value or in a proplist field? C4PropertyPath use_path; if (option.value_key.Get()) use_path = C4PropertyPath(property_path, option.value_key->GetCStr()); else use_path = property_path; // Value from a parameter or directly from the enum? if (option.adelegate) { // Value from a parameter. // Using a setter function? if (option.adelegate->GetSetFunction()) use_path = C4PropertyPath(use_path, option.adelegate->GetSetFunction(), C4PropertyPath::PPT_SetFunction); option.adelegate->SetModelData(editor->parameter_widget, use_path); } else { // No parameter. Use value. use_path.SetProperty(option.value); } } QWidget *C4PropertyDelegateEnum::CreateEditor(const C4PropertyDelegateFactory *parent_delegate, QWidget *parent, const QStyleOptionViewItem &option) const { Editor *editor = new Editor(parent); editor->layout = new QHBoxLayout(editor); editor->layout->setContentsMargins(0, 0, 0, 0); editor->layout->setMargin(0); editor->layout->setSpacing(0); editor->updating = true; editor->option_box = new QComboBox(editor); editor->layout->addWidget(editor->option_box); for (auto &option : options) editor->option_box->addItem(option.name->GetCStr()); void (QComboBox::*currentIndexChanged)(int) = &QComboBox::currentIndexChanged; connect(editor->option_box, currentIndexChanged, editor, [editor, this](int newval) { if (!editor->updating) this->UpdateOptionIndex(editor, newval); }); editor->updating = false; return editor; } void C4PropertyDelegateEnum::UpdateOptionIndex(C4PropertyDelegateEnum::Editor *editor, int newval) const { UpdateEditorParameter(editor); emit EditorValueChangedSignal(editor); } void C4PropertyDelegateEnum::EnsureOptionDelegateResolved(const Option &option) const { // Lazy-resolve parameter delegate if (!option.adelegate && option.adelegate_val.GetType() != C4V_Nil) option.adelegate = factory->GetDelegateByValue(option.adelegate_val); } QString C4PropertyDelegateEnum::GetDisplayString(const C4Value &v, class C4Object *obj) const { // Display string from value int32_t idx = GetOptionByValue(v); if (idx < 0) { // Value not found: Default display return C4PropertyDelegate::GetDisplayString(v, obj); } else { // Value found: Display option string plus parameter const Option &option = options[idx]; QString result = option.name->GetCStr(); // Lazy-resolve parameter delegate EnsureOptionDelegateResolved(option); if (option.adelegate) { C4Value param_val = v; if (option.value_key.Get()) { C4PropList *vp = v.getPropList(); if (vp) vp->GetPropertyByS(option.value_key, ¶m_val); } result += " "; result += option.adelegate->GetDisplayString(param_val, obj); } return result; } } const C4PropertyDelegateShape *C4PropertyDelegateEnum::GetShapeDelegate(const C4Value &val) const { // Does this delegate own a shape? Forward decision into selected option. int32_t option_idx = GetOptionByValue(val); if (option_idx < 0) return nullptr; const Option &option = options[option_idx]; EnsureOptionDelegateResolved(option); if (!option.adelegate) return nullptr; C4Value param_val = val; if (option.value_key.Get()) { C4PropList *vp = val.getPropList(); if (vp) vp->GetPropertyByS(option.value_key, ¶m_val); } return option.adelegate->GetShapeDelegate(param_val); } C4PropertyDelegateDef::C4PropertyDelegateDef(const C4PropertyDelegateFactory *factory, C4PropList *props) : C4PropertyDelegateEnum(factory, props) { // Collect sorted definitions std::vector defs = ::Definitions.GetAllDefs(props ? props->GetPropertyStr(P_Filter) : NULL); std::sort(defs.begin(), defs.end(), [](C4Def *a, C4Def *b) -> bool { return strcmp(a->GetName(), b->GetName()) < 0; }); // Add them ReserveOptions(defs.size() + 1); AddConstOption(::Strings.RegString("nil"), C4VNull); // nil is always an option for (C4Def *def : defs) { C4RefCntPointer option_name = ::Strings.RegString(FormatString("%s (%s)", def->id.ToString(), def->GetName())); AddConstOption(option_name, C4Value(def)); } } C4PropertyDelegateBool::C4PropertyDelegateBool(const C4PropertyDelegateFactory *factory, C4PropList *props) : C4PropertyDelegateEnum(factory, props) { // Add boolean options ReserveOptions(2); AddConstOption(::Strings.RegString(LoadResStr("IDS_CNS_FALSE")), C4VBool(false)); AddConstOption(::Strings.RegString(LoadResStr("IDS_CNS_TRUE")), C4VBool(true)); } bool C4PropertyDelegateBool::GetPropertyValue(C4PropList *props, C4String *key, C4Value *out_val) const { // Force value to bool props->GetPropertyByS(key, out_val); if (out_val->GetType() != C4V_Bool) *out_val = C4VBool(!!*out_val); return true; } C4PropertyDelegateHasEffect::C4PropertyDelegateHasEffect(const class C4PropertyDelegateFactory *factory, C4PropList *props) : C4PropertyDelegateBool(factory, props) { if (props) effect = props->GetPropertyStr(P_Effect); } bool C4PropertyDelegateHasEffect::GetPropertyValue(C4PropList *props, C4String *key, C4Value *out_val) const { const C4Object *obj = props->GetObject(); if (obj && effect) { bool has_effect = false; for (C4Effect *fx = obj->pEffects; fx; fx = fx->pNext) if (!fx->IsDead()) if (!strcmp(fx->GetName(), effect->GetCStr())) { has_effect = true; break; } *out_val = C4VBool(has_effect); return true; } return false; } C4PropertyDelegateC4ValueEnum::C4PropertyDelegateC4ValueEnum(const C4PropertyDelegateFactory *factory, C4PropList *props) : C4PropertyDelegateEnum(factory, props) { // Add default C4Value selections ReserveOptions(10); AddTypeOption(::Strings.RegString("nil"), C4V_Nil, C4VNull); AddTypeOption(::Strings.RegString("bool"), C4V_Bool, C4VNull, factory->GetDelegateByValue(C4VString("bool"))); AddTypeOption(::Strings.RegString("int"), C4V_Int, C4VNull, factory->GetDelegateByValue(C4VString("int"))); AddTypeOption(::Strings.RegString("string"), C4V_String, C4VNull, factory->GetDelegateByValue(C4VString("string"))); AddTypeOption(::Strings.RegString("array"), C4V_Array, C4VNull, factory->GetDelegateByValue(C4VString("array"))); AddTypeOption(::Strings.RegString("function"), C4V_Function, C4VNull, factory->GetDelegateByValue(C4VString("function"))); AddTypeOption(::Strings.RegString("object"), C4V_Object, C4VNull, factory->GetDelegateByValue(C4VString("object"))); AddTypeOption(::Strings.RegString("def"), C4V_Def, C4VNull, factory->GetDelegateByValue(C4VString("def"))); AddTypeOption(::Strings.RegString("effect"), C4V_Effect, C4VNull, factory->GetDelegateByValue(C4VString("effect"))); AddTypeOption(::Strings.RegString("proplist"), C4V_PropList, C4VNull, factory->GetDelegateByValue(C4VString("proplist"))); } C4PropertyDelegateC4ValueInputEditor::C4PropertyDelegateC4ValueInputEditor(QWidget *parent) : QWidget(parent), layout(NULL), edit(NULL), extended_button(NULL), commit_pending(false) { layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setMargin(0); layout->setSpacing(0); edit = new QLineEdit(this); layout->addWidget(edit); extended_button = new QPushButton("...", this); extended_button->setMaximumWidth(extended_button->fontMetrics().boundingRect("...").width() + 6); layout->addWidget(extended_button); extended_button->hide(); edit->setFocus(); setLayout(layout); } void C4PropertyDelegateC4ValueInput::SetEditorData(QWidget *aeditor, const C4Value &val, const C4PropertyPath &property_path) const { Editor *editor = static_cast(aeditor); editor->edit->setText(val.GetDataString().getData()); if (val.GetType() == C4V_PropList) { editor->extended_button->show(); editor->property_path = property_path; } else { editor->extended_button->hide(); } } void C4PropertyDelegateC4ValueInput::SetModelData(QObject *aeditor, const C4PropertyPath &property_path) const { // Only set model data when pressing Enter explicitely; not just when leaving Editor *editor = static_cast(aeditor); if (editor->commit_pending) { property_path.SetProperty(editor->edit->text().toUtf8()); editor->commit_pending = false; } } QWidget *C4PropertyDelegateC4ValueInput::CreateEditor(const class C4PropertyDelegateFactory *parent_delegate, QWidget *parent, const QStyleOptionViewItem &option) const { // Editor is just an edit box plus a "..." button for array/proplist types Editor *editor = new Editor(parent); // EditingDone only on Return; not just when leaving edit field connect(editor->edit, &QLineEdit::returnPressed, editor, [this, editor]() { editor->commit_pending = true; emit EditingDoneSignal(editor); }); connect(editor->extended_button, &QPushButton::pressed, editor, [this, editor]() { C4Value val = editor->property_path.ResolveValue(); C4PropList *props = val.getPropList(); if (props) { this->factory->GetPropertyModel()->DescendPath(props, props, editor->property_path); ::Console.EditCursor.InvalidateSelection(); } }); return editor; } /* Areas shown in viewport */ C4PropertyDelegateShape::C4PropertyDelegateShape(const class C4PropertyDelegateFactory *factory, C4PropList *props) : C4PropertyDelegate(factory, props), clr(0xffff0000), can_move_center(false) { if (props) { shape_type = props->GetPropertyStr(P_Type); clr = props->GetPropertyInt(P_Color) | 0xff000000; can_move_center = props->GetPropertyBool(P_CanMoveCenter); } } void C4PropertyDelegateShape::SetModelData(QObject *editor, const C4PropertyPath &property_path) const { C4ConsoleQtShape *shape = static_cast(editor); property_path.SetProperty(shape->GetValue()); } void C4PropertyDelegateShape::Paint(QPainter *painter, const QStyleOptionViewItem &option, const C4Value &val) const { // Background color if (option.state & QStyle::State_Selected) painter->fillRect(option.rect, option.palette.highlight()); else painter->fillRect(option.rect, option.palette.base()); // Draw a frame in shape color painter->save(); QColor frame_color = QColor(QRgb(clr & 0xffffff)); int32_t width = Clamp(option.rect.height() / 8, 2, 6) &~1; QPen rect_pen(QBrush(frame_color), width, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin); painter->setPen(rect_pen); QRect inner_rect = option.rect.adjusted(width / 2, width / 2, -width / 2, -width / 2); if (shape_type && shape_type->GetData() == "circle") { painter->drawEllipse(inner_rect); if (can_move_center) painter->drawPoint(inner_rect.center()); } else { painter->drawRect(inner_rect); } painter->restore(); } /* Delegate factory: Create delegates based on the C4Value type */ C4PropertyDelegate *C4PropertyDelegateFactory::CreateDelegateByString(const C4String *str, C4PropList *props) const { // safety if (!str) return NULL; // create default base types if (str->GetData() == "int") return new C4PropertyDelegateInt(this, props); if (str->GetData() == "color") return new C4PropertyDelegateColor(this, props); if (str->GetData() == "def") return new C4PropertyDelegateDef(this, props); if (str->GetData() == "enum") return new C4PropertyDelegateEnum(this, props); if (str->GetData() == "bool") return new C4PropertyDelegateBool(this, props); if (str->GetData() == "has_effect") return new C4PropertyDelegateHasEffect(this, props); if (str->GetData() == "c4valueenum") return new C4PropertyDelegateC4ValueEnum(this, props); if (str->GetData() == "rect" || str->GetData() == "circle") return new C4PropertyDelegateShape(this, props); if (str->GetData() == "any") return new C4PropertyDelegateC4ValueInput(this, props); // unknown type return NULL; } C4PropertyDelegate *C4PropertyDelegateFactory::CreateDelegateByValue(const C4Value &val) const { switch (val.GetType()) { case C4V_Nil: return new C4PropertyDelegateC4ValueInput(this, NULL); case C4V_Array: return new C4PropertyDelegateEnum(this, NULL, val.getArray()); case C4V_PropList: { C4PropList *props = val._getPropList(); if (!props) break; return CreateDelegateByString(props->GetPropertyStr(P_Type), props); } case C4V_String: return CreateDelegateByString(val._getStr(), NULL); default: // Invalid delegte: No editor. break; } return NULL; } C4PropertyDelegate *C4PropertyDelegateFactory::GetDelegateByValue(const C4Value &val) const { auto iter = delegates.find(val); if (iter != delegates.end()) return iter->second.get(); C4PropertyDelegate *new_delegate = CreateDelegateByValue(val); delegates.insert(std::make_pair(val, std::unique_ptr(new_delegate))); return new_delegate; } C4PropertyDelegate *C4PropertyDelegateFactory::GetDelegateByIndex(const QModelIndex &index) const { C4ConsoleQtPropListModel::Property *prop = static_cast(index.internalPointer()); if (!prop) return NULL; if (!prop->delegate) prop->delegate = GetDelegateByValue(prop->delegate_info); return prop->delegate; } void C4PropertyDelegateFactory::ClearDelegates() { delegates.clear(); } void C4PropertyDelegateFactory::EditorValueChanged(QWidget *editor) { emit commitData(editor); } void C4PropertyDelegateFactory::EditingDone(QWidget *editor) { emit commitData(editor); //emit closeEditor(editor); - done by qt somewhere else... } void C4PropertyDelegateFactory::setEditorData(QWidget *editor, const QModelIndex &index) const { // Put property value from proplist into editor C4PropertyDelegate *d = GetDelegateByIndex(index); if (!d) return; // Fetch property only first time - ignore further updates to simplify editing C4ConsoleQtPropListModel::Property *prop = static_cast(index.internalPointer()); if (!prop || !prop->about_to_edit) return; prop->about_to_edit = false; C4Value val; C4PropList *props = prop->parent_proplist.getPropList(); if (props) { d->GetPropertyValue(props, prop->key, &val); d->SetEditorData(editor, val, d->GetPathForProperty(prop)); } } void C4PropertyDelegateFactory::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { // Fetch property value from editor and set it into proplist C4PropertyDelegate *d = GetDelegateByIndex(index); if (!d) return; C4ConsoleQtPropListModel::Property *prop = static_cast(index.internalPointer()); SetPropertyData(d, editor, prop); } void C4PropertyDelegateFactory::SetPropertyData(const C4PropertyDelegate *d, QObject *editor, C4ConsoleQtPropListModel::Property *editor_prop) const { // Safety: Ensure target properties still exist C4PropList *target_props = editor_prop->parent_proplist.getPropList(); if (!target_props) return; // Set according to delegate d->SetModelData(editor, d->GetPathForProperty(editor_prop)); } QWidget *C4PropertyDelegateFactory::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { C4PropertyDelegate *d = GetDelegateByIndex(index); if (!d) return NULL; C4ConsoleQtPropListModel::Property *prop = static_cast(index.internalPointer()); prop->about_to_edit = true; QWidget *editor = d->CreateEditor(this, parent, option); // Connect value change signals (if editing is possible for this property) // For some reason, commitData needs a non-const pointer if (editor) { connect(d, &C4PropertyDelegate::EditorValueChangedSignal, editor, [editor, this](QWidget *signal_editor) { if (signal_editor == editor) const_cast(this)->EditorValueChanged(editor); }); connect(d, &C4PropertyDelegate::EditingDoneSignal, editor, [editor, this](QWidget *signal_editor) { if (signal_editor == editor) const_cast(this)->EditingDone(editor); }); } current_editor = editor; return editor; } void C4PropertyDelegateFactory::destroyEditor(QWidget *editor, const QModelIndex &index) const { if (editor == current_editor) current_editor = nullptr; QStyledItemDelegate::destroyEditor(editor, index); } void C4PropertyDelegateFactory::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const { C4PropertyDelegate *d = GetDelegateByIndex(index); if (!d) return; return d->UpdateEditorGeometry(editor, option); } QSize C4PropertyDelegateFactory::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { int height = QApplication::fontMetrics().height() + 4; return QSize(100, height); } void C4PropertyDelegateFactory::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { // Delegate has custom painting? C4ConsoleQtPropListModel::Property *prop = static_cast(index.internalPointer()); C4PropertyDelegate *d = GetDelegateByIndex(index); if (d && prop && d->HasCustomPaint()) { C4Value val; C4PropList *props = prop->parent_proplist.getPropList(); if (props) { d->GetPropertyValue(props, prop->key, &val); d->Paint(painter, option, val); return; } } // Otherwise use default paint implementation QStyledItemDelegate::paint(painter, option, index); } void C4PropertyDelegateFactory::OnPropListChanged() { if (current_editor) emit closeEditor(current_editor); } /* Proplist table view */ C4ConsoleQtPropListModel::C4ConsoleQtPropListModel(C4PropertyDelegateFactory *delegate_factory) : delegate_factory(delegate_factory) { header_font.setBold(true); } C4ConsoleQtPropListModel::~C4ConsoleQtPropListModel() { } bool C4ConsoleQtPropListModel::AddPropertyGroup(C4PropList *add_proplist, int32_t group_index, QString name, C4PropList *target_proplist, C4Object *base_effect_object) { const char *editor_prop_prefix = "EditorProp_"; auto new_properties = add_proplist->GetSortedLocalProperties(editor_prop_prefix, target_proplist); if (!new_properties.size()) return false; if (property_groups.size() == group_index) property_groups.resize(group_index + 1); PropertyGroup &properties = property_groups[group_index]; C4PropListStatic *proplist_static = add_proplist->IsStatic(); properties.name = name; properties.props.resize(new_properties.size()); for (int32_t i = 0; i < new_properties.size(); ++i) { Property *prop = &properties.props[i]; // Property access path prop->parent_proplist.SetPropList(target_proplist); if (base_effect_object) // Access to effect prop->property_path = C4PropertyPath(target_proplist->GetEffect(), base_effect_object); else prop->property_path = target_path; // Property data prop->key = NULL; prop->display_name = NULL; prop->delegate_info.Set0(); // default C4Value delegate prop->group_idx = group_index; C4Value published_prop_val; add_proplist->GetPropertyByS(new_properties[i], &published_prop_val); C4PropList *published_prop = published_prop_val.getPropList(); if (published_prop) { prop->key = published_prop->GetPropertyStr(P_Key); prop->display_name = published_prop->GetPropertyStr(P_Name); prop->delegate_info.SetPropList(published_prop); } if (!prop->key) properties.props[i].key = ::Strings.RegString(new_properties[i]->GetCStr() + strlen(editor_prop_prefix)); if (!prop->display_name) properties.props[i].display_name = ::Strings.RegString(new_properties[i]->GetCStr() + strlen(editor_prop_prefix)); prop->delegate = delegate_factory->GetDelegateByValue(prop->delegate_info); C4Value v; prop->delegate->GetPropertyValue(target_proplist, prop->key, &v); // Connect editable shape to property const C4PropertyDelegateShape *new_shape_delegate = prop->delegate->GetShapeDelegate(v); if (new_shape_delegate != prop->shape_delegate) { prop->shape_delegate = new_shape_delegate; if (new_shape_delegate) { C4ConsoleQtShape *shape = ::Console.EditCursor.GetShapes()->CreateShape(target_proplist->GetObject(), published_prop, v); C4PropertyDelegateFactory *factory = this->delegate_factory; connect(shape, &C4ConsoleQtShape::ShapeDragged, new_shape_delegate, [factory, new_shape_delegate, shape, prop]() { factory->SetPropertyData(new_shape_delegate, shape, prop); }); prop->shape.Set(shape); } } } return true; } void C4ConsoleQtPropListModel::SetBasePropList(C4PropList *new_proplist) { // Clear stack and select new proplist // Update properties target_proplist.SetPropList(new_proplist); base_proplist.SetPropList(new_proplist); // objects derive their custom properties info_proplist.SetPropList(target_proplist.getObj()); target_path = C4PropertyPath(new_proplist); target_path_stack.clear(); UpdatePropList(); } void C4ConsoleQtPropListModel::DescendPath(C4PropList *new_proplist, C4PropList *new_info_proplist, const C4PropertyPath &new_path) { // Add previous proplist to stack target_path_stack.push_back(TargetStackEntry(target_path, target_proplist, info_proplist)); // descend target_proplist.SetPropList(new_proplist); info_proplist.SetPropList(new_info_proplist); target_path = new_path; UpdatePropList(); } void C4ConsoleQtPropListModel::AscendPath() { // Go up in target stack (if possible) for (;;) { if (!target_path_stack.size()) { SetBasePropList(nullptr); return; } TargetStackEntry entry = target_path_stack.back(); target_path_stack.pop_back(); if (!entry.value || !entry.info_proplist) continue; // property was removed; go up further in stack // Safety: Make sure we're still on the same value C4Value target = entry.path.ResolveValue(); if (!target.IsIdenticalTo(entry.value)) continue; // Set new value target_path = entry.path; target_proplist = entry.value; info_proplist = entry.info_proplist; UpdatePropList(); break; } } void C4ConsoleQtPropListModel::UpdatePropList() { // Safe-get from C4Values in case any prop lists got deleted C4PropList *target_proplist = this->target_proplist.getPropList(); C4PropList *base_proplist = this->base_proplist.getPropList(); C4PropList *info_proplist = this->info_proplist.getPropList(); // Determine number of property groups int32_t num_groups = 0; if (target_proplist) { // Published properties if (info_proplist) { C4Object *obj = info_proplist->GetObject(); // Properties from effects (no inheritance supported) if (obj) { for (C4Effect *fx = obj->pEffects; fx; fx = fx->pNext) { QString name = fx->GetName(); if (AddPropertyGroup(fx, num_groups, name, fx, obj)) ++num_groups; } } // Properties from object (but not on definition) if (obj || !info_proplist->GetDef()) { for (C4PropList *check_proplist = info_proplist; check_proplist; check_proplist = check_proplist->GetPrototype()) { QString name; C4PropListStatic *proplist_static = check_proplist->IsStatic(); if (proplist_static) name = QString(proplist_static->GetDataString().getData()); else name = check_proplist->GetName(); if (AddPropertyGroup(check_proplist, num_groups, name, target_proplist, nullptr)) ++num_groups; } } // properties from global list for objects if (obj) { C4Def *editor_base = C4Id2Def(C4ID::EditorBase); if (editor_base) if (AddPropertyGroup(editor_base, num_groups, LoadResStr("IDS_CNS_OBJECT"), target_proplist, nullptr)) ++num_groups; } } // Always: Internal properties auto new_properties = target_proplist->GetSortedLocalProperties(); if (property_groups.size() == num_groups) property_groups.resize(num_groups + 1); PropertyGroup &internal_properties = property_groups[num_groups]; internal_properties.name = LoadResStr("IDS_CNS_INTERNAL"); internal_properties.props.resize(new_properties.size()); for (int32_t i = 0; i < new_properties.size(); ++i) { internal_properties.props[i].parent_proplist = this->target_proplist; internal_properties.props[i].key = new_properties[i]; internal_properties.props[i].display_name = new_properties[i]; internal_properties.props[i].delegate_info.Set0(); // default C4Value delegate internal_properties.props[i].delegate = NULL; // init when needed internal_properties.props[i].group_idx = num_groups; internal_properties.props[i].shape.Clear(); internal_properties.props[i].shape_delegate = nullptr; } ++num_groups; } property_groups.resize(num_groups); QModelIndex topLeft = index(0, 0, QModelIndex()); QModelIndex bottomRight = index(rowCount() - 1, columnCount() - 1, QModelIndex()); delegate_factory->OnPropListChanged(); emit dataChanged(topLeft, bottomRight); emit layoutChanged(); } int C4ConsoleQtPropListModel::rowCount(const QModelIndex & parent) const { // Nothing loaded? if (!target_proplist.getPropList()) return 0; // Top level: Property groups if (!parent.isValid()) return property_groups.size(); // Mid level: Descend into property lists QModelIndex grandparent = parent.parent(); if (!grandparent.isValid()) { if (parent.row() >= 0 && parent.row() < property_groups.size()) return property_groups[parent.row()].props.size(); } return 0; } int C4ConsoleQtPropListModel::columnCount(const QModelIndex & parent) const { return 2; // Name + Data } QVariant C4ConsoleQtPropListModel::headerData(int section, Qt::Orientation orientation, int role) const { // Table headers if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { if (section == 0) return QVariant(LoadResStr("IDS_CTL_NAME")); if (section == 1) return QVariant(LoadResStr("IDS_CNS_VALUE")); } return QVariant(); } QVariant C4ConsoleQtPropListModel::data(const QModelIndex & index, int role) const { // Anything loaded? C4PropList *props = target_proplist.getPropList(); if (!props) return QVariant(); // Headers QModelIndex parent = index.parent(); if (!parent.isValid()) { if (!index.column()) { if (role == Qt::DisplayRole) { if (index.row() >= 0 && index.row() < property_groups.size()) return property_groups[index.row()].name; } else if (role == Qt::FontRole) { return header_font; } } return QVariant(); } // Query latest data from prop list Property *prop = static_cast(index.internalPointer()); if (!prop) return QVariant(); if (!prop->delegate) prop->delegate = delegate_factory->GetDelegateByValue(prop->delegate_info); if (role == Qt::DisplayRole) { switch (index.column()) { case 0: // First col: Property Name return QVariant(prop->display_name->GetCStr()); case 1: // Second col: Property value { C4Value v; prop->delegate->GetPropertyValue(props, prop->key, &v); return QVariant(prop->delegate->GetDisplayString(v, props->GetObject())); } } } else if (role == Qt::BackgroundColorRole && index.column()==1) { C4Value v; prop->delegate->GetPropertyValue(props, prop->key, &v); QColor bgclr = prop->delegate->GetDisplayBackgroundColor(v, props->GetObject()); if (bgclr.isValid()) return bgclr; } else if (role == Qt::TextColorRole && index.column() == 1) { C4Value v; prop->delegate->GetPropertyValue(props, prop->key, &v); QColor txtclr = prop->delegate->GetDisplayTextColor(v, props->GetObject()); if (txtclr.isValid()) return txtclr; } // Nothing to show return QVariant(); } QModelIndex C4ConsoleQtPropListModel::index(int row, int column, const QModelIndex &parent) const { if (column < 0 || column > 1) return QModelIndex(); // Top level index? if (!parent.isValid()) { // Top level has headers only if (row < 0 || row >= property_groups.size()) return QModelIndex(); return createIndex(row, column, nullptr); } // Property? QModelIndex grandparent = parent.parent(); if (!grandparent.isValid()) { const PropertyGroup *property_group = NULL; if (parent.row() >= 0 && parent.row() < property_groups.size()) { property_group = &property_groups[parent.row()]; if (row < 0 || row >= property_group->props.size()) return QModelIndex(); const Property * prop = &(property_group->props[row]); return createIndex(row, column, const_cast(prop)); } } return QModelIndex(); } QModelIndex C4ConsoleQtPropListModel::parent(const QModelIndex &index) const { Property *prop = static_cast(index.internalPointer()); if (!prop) return QModelIndex(); // Find list to use return createIndex(prop->group_idx, 0, nullptr); } Qt::ItemFlags C4ConsoleQtPropListModel::flags(const QModelIndex &index) const { Qt::ItemFlags flags = QAbstractItemModel::flags(index); if (index.isValid() && index.column() == 1 && index.internalPointer()) { Property *prop = static_cast(index.internalPointer()); C4PropList *parent_proplist = prop->parent_proplist.getPropList(); // Only object properties are editable at the moment if (parent_proplist && !parent_proplist->IsFrozen() && (parent_proplist->GetObject()==parent_proplist)) flags |= Qt::ItemIsEditable; else flags &= ~Qt::ItemIsEnabled; } return flags; }