1 /*
2 * OpenClonk, http://www.openclonk.org
3 *
4 * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
5 * Copyright (c) 2013, The OpenClonk Team and contributors
6 *
7 * Distributed under the terms of the ISC license; see accompanying file
8 * "COPYING" for details.
9 *
10 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11 * See accompanying file "TRADEMARK" for details.
12 *
13 * To redistribute this file separately, substitute the full license texts
14 * for the above references.
15 */
16 
17 #include "C4Include.h"
18 #include "script/C4Value.h"
19 #include "editor/C4ConsoleQtPropListViewer.h"
20 #include "editor/C4ConsoleQtDefinitionListViewer.h"
21 #include "editor/C4ConsoleQtState.h"
22 #include "editor/C4ConsoleQtLocalizeString.h"
23 #include "editor/C4Console.h"
24 #include "object/C4Object.h"
25 #include "object/C4GameObjects.h"
26 #include "object/C4DefList.h"
27 #include "object/C4Def.h"
28 #include "script/C4Effect.h"
29 #include "script/C4AulExec.h"
30 #include "platform/C4SoundInstance.h"
31 
32 
33 /* Property delegate base class */
34 
C4PropertyDelegate(const C4PropertyDelegateFactory * factory,C4PropList * props)35 C4PropertyDelegate::C4PropertyDelegate(const C4PropertyDelegateFactory *factory, C4PropList *props)
36 	: QObject(), factory(factory), set_function_type(C4PropertyPath::PPT_SetFunction)
37 {
38 	// Resolve getter+setter callback names
39 	if (props)
40 	{
41 		creation_props = C4VPropList(props);
42 		name = props->GetPropertyStr(P_Name);
43 		set_function = props->GetPropertyStr(P_Set);
44 		if (props->GetPropertyBool(P_SetGlobal))
45 		{
46 			set_function_type = C4PropertyPath::PPT_GlobalSetFunction;
47 		}
48 		else if (props->GetPropertyBool(P_SetRoot))
49 		{
50 			set_function_type = C4PropertyPath::PPT_RootSetFunction;
51 		}
52 		else
53 		{
54 			set_function_type = C4PropertyPath::PPT_SetFunction;
55 		}
56 		async_get_function = props->GetPropertyStr(P_AsyncGet);
57 		update_callback = props->GetPropertyStr(P_OnUpdate);
58 	}
59 }
60 
UpdateEditorGeometry(QWidget * editor,const QStyleOptionViewItem & option) const61 void C4PropertyDelegate::UpdateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option) const
62 {
63 	editor->setGeometry(option.rect);
64 }
65 
GetPropertyValueBase(const C4Value & container,C4String * key,int32_t index,C4Value * out_val) const66 bool C4PropertyDelegate::GetPropertyValueBase(const C4Value &container, C4String *key, int32_t index, C4Value *out_val) const
67 {
68 	switch (container.GetType())
69 	{
70 	case C4V_PropList:
71 		return container._getPropList()->GetPropertyByS(key, out_val);
72 	case C4V_Array:
73 		*out_val = container._getArray()->GetItem(index);
74 		return true;
75 	default:
76 		return false;
77 	}
78 }
79 
GetPropertyValue(const C4Value & container,C4String * key,int32_t index,C4Value * out_val) const80 bool C4PropertyDelegate::GetPropertyValue(const C4Value &container, C4String *key, int32_t index, C4Value *out_val) const
81 {
82 	if (async_get_function)
83 	{
84 		C4PropList *props = container.getPropList();
85 		if (props)
86 		{
87 			*out_val = props->Call(async_get_function.Get());
88 			return true;
89 		}
90 		return false;
91 	}
92 	else
93 	{
94 		return GetPropertyValueBase(container, key, index, out_val);
95 	}
96 }
97 
GetDisplayString(const C4Value & v,C4Object * obj,bool short_names) const98 QString C4PropertyDelegate::GetDisplayString(const C4Value &v, C4Object *obj, bool short_names) const
99 {
100 	return QString(v.GetDataString().getData());
101 }
102 
GetDisplayTextColor(const C4Value & val,class C4Object * obj) const103 QColor C4PropertyDelegate::GetDisplayTextColor(const C4Value &val, class C4Object *obj) const
104 {
105 	return QColor(); // invalid = default
106 }
107 
GetDisplayBackgroundColor(const C4Value & val,class C4Object * obj) const108 QColor C4PropertyDelegate::GetDisplayBackgroundColor(const C4Value &val, class C4Object *obj) const
109 {
110 	return QColor(); // invalid = default
111 }
112 
GetPathForProperty(C4ConsoleQtPropListModelProperty * editor_prop) const113 C4PropertyPath C4PropertyDelegate::GetPathForProperty(C4ConsoleQtPropListModelProperty *editor_prop) const
114 {
115 	C4PropertyPath path;
116 	if (editor_prop->property_path.IsEmpty())
117 		path = C4PropertyPath(editor_prop->parent_value.getPropList());
118 	else
119 		path = editor_prop->property_path;
120 	return GetPathForProperty(path, editor_prop->key ? editor_prop->key->GetCStr() : nullptr);
121 }
122 
GetPathForProperty(const C4PropertyPath & parent_path,const char * default_subpath) const123 C4PropertyPath C4PropertyDelegate::GetPathForProperty(const C4PropertyPath &parent_path, const char *default_subpath) const
124 {
125 	// Get path
126 	C4PropertyPath subpath;
127 	if (default_subpath && *default_subpath)
128 		subpath = C4PropertyPath(parent_path, default_subpath);
129 	else
130 		subpath = parent_path;
131 	// Set path
132 	if (GetSetFunction())
133 	{
134 		subpath.SetSetPath(parent_path, GetSetFunction(), set_function_type);
135 	}
136 	return subpath;
137 }
138 
139 
140 /* Integer delegate */
141 
C4PropertyDelegateInt(const C4PropertyDelegateFactory * factory,C4PropList * props)142 C4PropertyDelegateInt::C4PropertyDelegateInt(const C4PropertyDelegateFactory *factory, C4PropList *props)
143 	: C4PropertyDelegate(factory, props), min(std::numeric_limits<int32_t>::min()), max(std::numeric_limits<int32_t>::max()), step(1)
144 {
145 	if (props)
146 	{
147 		min = props->GetPropertyInt(P_Min, min);
148 		max = props->GetPropertyInt(P_Max, max);
149 		step = props->GetPropertyInt(P_Step, step);
150 	}
151 }
152 
SetEditorData(QWidget * editor,const C4Value & val,const C4PropertyPath & property_path) const153 void C4PropertyDelegateInt::SetEditorData(QWidget *editor, const C4Value &val, const C4PropertyPath &property_path) const
154 {
155 	QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
156 	spinBox->setValue(val.getInt());
157 }
158 
SetModelData(QObject * editor,const C4PropertyPath & property_path,C4ConsoleQtShape * prop_shape) const159 void C4PropertyDelegateInt::SetModelData(QObject *editor, const C4PropertyPath &property_path, C4ConsoleQtShape *prop_shape) const
160 {
161 	QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
162 	spinBox->interpretText();
163 	property_path.SetProperty(C4VInt(spinBox->value()));
164 	factory->GetPropertyModel()->DoOnUpdateCall(property_path, this);
165 }
166 
CreateEditor(const C4PropertyDelegateFactory * parent_delegate,QWidget * parent,const QStyleOptionViewItem & option,bool by_selection,bool is_child) const167 QWidget *C4PropertyDelegateInt::CreateEditor(const C4PropertyDelegateFactory *parent_delegate, QWidget *parent, const QStyleOptionViewItem &option, bool by_selection, bool is_child) const
168 {
169 	QSpinBox *editor = new QSpinBox(parent);
170 	editor->setMinimum(min);
171 	editor->setMaximum(max);
172 	editor->setSingleStep(step);
173 	connect(editor, &QSpinBox::editingFinished, this, [editor, this]() {
174 		emit EditingDoneSignal(editor);
175 	});
176 	// Selection in child enum: Direct focus
177 	if (by_selection && is_child) editor->setFocus();
178 	return editor;
179 }
180 
IsPasteValid(const C4Value & val) const181 bool C4PropertyDelegateInt::IsPasteValid(const C4Value &val) const
182 {
183 	// Check int type and limits
184 	if (val.GetType() != C4V_Int) return false;
185 	int32_t ival = val._getInt();
186 	return (ival >= min && ival <= max);
187 }
188 
189 
190 /* String delegate */
191 
C4PropertyDelegateStringEditor(QWidget * parent,bool has_localization_button)192 C4PropertyDelegateStringEditor::C4PropertyDelegateStringEditor(QWidget *parent, bool has_localization_button)
193 	: QWidget(parent), edit(nullptr), localization_button(nullptr), commit_pending(false), text_edited(false)
194 {
195 	auto layout = new QHBoxLayout(this);
196 	layout->setContentsMargins(0, 0, 0, 0);
197 	layout->setMargin(0);
198 	layout->setSpacing(0);
199 	edit = new QLineEdit(this);
200 	layout->addWidget(edit);
201 	if (has_localization_button)
202 	{
203 		localization_button = new QPushButton(QString(LoadResStr("IDS_CNS_MORE")), this);
204 		layout->addWidget(localization_button);
205 		connect(localization_button, &QPushButton::pressed, this, [this]() {
206 			// Show dialogue
207 			OpenLocalizationDialogue();
208 		});
209 	}
210 	connect(edit, &QLineEdit::returnPressed, this, [this]() {
211 		text_edited = true;
212 		commit_pending = true;
213 		emit EditingDoneSignal();
214 	});
215 	connect(edit, &QLineEdit::textEdited, this, [this]() {
216 		text_edited = true;
217 		commit_pending = true;
218 	});
219 }
220 
OpenLocalizationDialogue()221 void C4PropertyDelegateStringEditor::OpenLocalizationDialogue()
222 {
223 	if (!localization_dialogue)
224 	{
225 		// Make sure we have an updated value
226 		StoreEditedText();
227 		// Make sure we're using a localized string
228 		if (value.GetType() != C4V_PropList)
229 		{
230 			C4PropList *value_proplist = ::Game.AllocateTranslatedString();
231 			if (value.GetType() == C4V_String)
232 			{
233 				C4String *lang = ::Strings.RegString(lang_code);
234 				value_proplist->SetPropertyByS(lang, value);
235 			}
236 			value = C4VPropList(value_proplist);
237 		}
238 		// Open dialogue on value
239 		localization_dialogue.reset(new C4ConsoleQtLocalizeStringDlg(::Console.GetState()->window.get(), value));
240 		connect(localization_dialogue.get(), &C4ConsoleQtLocalizeStringDlg::accepted, this, [this]() {
241 			// Usually, the proplist owned by localization_dialogue is the same as this->value
242 			// However, it may have changed if there was an update call that modified the value while the dialogue was open
243 			// In this case, take the value from the dialogue
244 			SetValue(C4VPropList(localization_dialogue->GetTranslations()));
245 			// Finish editing on the value
246 			CloseLocalizationDialogue();
247 			commit_pending = true;
248 			emit EditingDoneSignal();
249 		});
250 		connect(localization_dialogue.get(), &C4ConsoleQtLocalizeStringDlg::rejected, this, [this]() {
251 			CloseLocalizationDialogue();
252 		});
253 		localization_dialogue->show();
254 	}
255 }
256 
CloseLocalizationDialogue()257 void C4PropertyDelegateStringEditor::CloseLocalizationDialogue()
258 {
259 	if (localization_dialogue)
260 	{
261 		localization_dialogue->close();
262 		localization_dialogue.reset();
263 	}
264 }
265 
StoreEditedText()266 void C4PropertyDelegateStringEditor::StoreEditedText()
267 {
268 	if (text_edited)
269 	{
270 		// TODO: Would be better to handle escaping in the C4Value-to-string code
271 		QString new_value = edit->text();
272 		new_value = new_value.replace(R"(\)", R"(\\)").replace(R"(")", R"(\")");
273 		C4Value text_value = C4VString(new_value.toUtf8());
274 		// If translatable, always store as translation proplist
275 		// This makes it easier to collect strings to be localized in the localization overview
276 		if (localization_button)
277 		{
278 			C4PropList *value_proplist = this->value.getPropList();
279 			if (!value_proplist)
280 			{
281 				value_proplist = ::Game.AllocateTranslatedString();
282 			}
283 			C4String *lang = ::Strings.RegString(lang_code);
284 			value_proplist->SetPropertyByS(lang, text_value);
285 		}
286 		else
287 		{
288 			this->value = text_value;
289 		}
290 		text_edited = false;
291 	}
292 }
293 
SetValue(const C4Value & val)294 void C4PropertyDelegateStringEditor::SetValue(const C4Value &val)
295 {
296 	// Set editor text to value
297 	// Resolve text string and default language for localized strings
298 	C4String *s;
299 	C4Value language;
300 	if (localization_button)
301 	{
302 		s = ::Game.GetTranslatedString(val, &language, true);
303 		C4String *language_string = language.getStr();
304 		SCopy(language_string ? language_string->GetCStr() : Config.General.LanguageEx, lang_code, 2);
305 		QFontMetrics fm(localization_button->font());
306 		localization_button->setFixedWidth(fm.width(lang_code) + 4);
307 		localization_button->setText(QString(lang_code));
308 	}
309 	else
310 	{
311 		s = val.getStr();
312 	}
313 	edit->setText(QString(s ? s->GetCStr() : ""));
314 	// Remember full value with all localizations
315 	if (val.GetType() == C4V_PropList)
316 	{
317 		if (val != this->value)
318 		{
319 			// Localization proplist: Create a copy (C4Value::Copy() would be nice)
320 			C4PropList *new_value_proplist = new C4PropListScript();
321 			this->value = C4VPropList(new_value_proplist);
322 			C4PropList *val_proplist = val.getPropList();
323 			for (C4String *lang : val_proplist->GetSortedLocalProperties())
324 			{
325 				C4Value lang_string;
326 				val_proplist->GetPropertyByS(lang, &lang_string);
327 				new_value_proplist->SetPropertyByS(lang, lang_string);
328 			}
329 		}
330 	}
331 	else
332 	{
333 		this->value = val;
334 	}
335 }
336 
GetValue()337 C4Value C4PropertyDelegateStringEditor::GetValue()
338 {
339 	// Flush edits from the text field into value
340 	StoreEditedText();
341 	// Return current value
342 	return this->value;
343 }
344 
C4PropertyDelegateString(const C4PropertyDelegateFactory * factory,C4PropList * props)345 C4PropertyDelegateString::C4PropertyDelegateString(const C4PropertyDelegateFactory *factory, C4PropList *props)
346 	: C4PropertyDelegate(factory, props)
347 {
348 	if (props)
349 	{
350 		translatable = props->GetPropertyBool(P_Translatable);
351 	}
352 }
353 
SetEditorData(QWidget * aeditor,const C4Value & val,const C4PropertyPath & property_path) const354 void C4PropertyDelegateString::SetEditorData(QWidget *aeditor, const C4Value &val, const C4PropertyPath &property_path) const
355 {
356 	Editor *editor = static_cast<Editor*>(aeditor);
357 	editor->SetValue(val);
358 }
359 
SetModelData(QObject * aeditor,const C4PropertyPath & property_path,C4ConsoleQtShape * prop_shape) const360 void C4PropertyDelegateString::SetModelData(QObject *aeditor, const C4PropertyPath &property_path, C4ConsoleQtShape *prop_shape) const
361 {
362 	Editor *editor = static_cast<Editor*>(aeditor);
363 	// Only set model data when pressing Enter explicitely; not just when leaving
364 	if (editor->IsCommitPending())
365 	{
366 		property_path.SetProperty(editor->GetValue());
367 		factory->GetPropertyModel()->DoOnUpdateCall(property_path, this);
368 		editor->SetCommitPending(false);
369 	}
370 }
371 
CreateEditor(const C4PropertyDelegateFactory * parent_delegate,QWidget * parent,const QStyleOptionViewItem & option,bool by_selection,bool is_child) const372 QWidget *C4PropertyDelegateString::CreateEditor(const C4PropertyDelegateFactory *parent_delegate, QWidget *parent, const QStyleOptionViewItem &option, bool by_selection, bool is_child) const
373 {
374 	Editor *editor = new Editor(parent, translatable);
375 	// EditingDone on return or when leaving edit field after a change has been made
376 	connect(editor, &Editor::EditingDoneSignal, editor, [this, editor]() {
377 		emit EditingDoneSignal(editor);
378 	});
379 	// Selection in child enum: Direct focus
380 	if (by_selection && is_child) editor->setFocus();
381 	return editor;
382 }
383 
GetDisplayString(const C4Value & v,C4Object * obj,bool short_names) const384 QString C4PropertyDelegateString::GetDisplayString(const C4Value &v, C4Object *obj, bool short_names) const
385 {
386 	// Raw string without ""
387 	C4String *s = translatable ? ::Game.GetTranslatedString(v, nullptr, true) : v.getStr();
388 	return QString(s ? s->GetCStr() : "");
389 }
390 
IsPasteValid(const C4Value & val) const391 bool C4PropertyDelegateString::IsPasteValid(const C4Value &val) const
392 {
393 	// Check string type or translatable proplist
394 	if (val.GetType() == C4V_String) return true;
395 	if (translatable)
396 	{
397 		C4PropList *val_p = val.getPropList();
398 		if (val_p)
399 		{
400 			return val_p->GetPropertyStr(P_Function) == &::Strings.P[P_Translate];
401 		}
402 	}
403 	return false;
404 }
405 
406 
407 /* Delegate editor: Text left and button right */
408 
C4PropertyDelegateLabelAndButtonWidget(QWidget * parent)409 C4PropertyDelegateLabelAndButtonWidget::C4PropertyDelegateLabelAndButtonWidget(QWidget *parent)
410 	: QWidget(parent), layout(nullptr), label(nullptr), button(nullptr), button_pending(false)
411 {
412 	layout = new QHBoxLayout(this);
413 	layout->setContentsMargins(0, 0, 0, 0);
414 	layout->setMargin(0);
415 	layout->setSpacing(0);
416 	label = new QLabel(this);
417 	QPalette palette = label->palette();
418 	palette.setColor(label->foregroundRole(), palette.color(QPalette::HighlightedText));
419 	palette.setColor(label->backgroundRole(), palette.color(QPalette::Highlight));
420 	label->setPalette(palette);
421 	layout->addWidget(label);
422 	button = new QPushButton(QString(LoadResStr("IDS_CNS_MORE")), this);
423 	layout->addWidget(button);
424 	// Make sure to draw over view in background
425 	setPalette(palette);
426 	setAutoFillBackground(true);
427 }
428 
429 
430 /* Descend path delegate base class for arrays and proplist */
431 
C4PropertyDelegateDescendPath(const class C4PropertyDelegateFactory * factory,C4PropList * props)432 C4PropertyDelegateDescendPath::C4PropertyDelegateDescendPath(const class C4PropertyDelegateFactory *factory, C4PropList *props)
433 	: C4PropertyDelegate(factory, props), edit_on_selection(true)
434 {
435 	if (props)
436 	{
437 		info_proplist = C4VPropList(props); // Descend info is this definition
438 		edit_on_selection = props->GetPropertyBool(P_EditOnSelection, edit_on_selection);
439 		descend_path = props->GetPropertyStr(P_DescendPath);
440 	}
441 }
442 
SetEditorData(QWidget * aeditor,const C4Value & val,const C4PropertyPath & property_path) const443 void C4PropertyDelegateDescendPath::SetEditorData(QWidget *aeditor, const C4Value &val, const C4PropertyPath &property_path) const
444 {
445 	Editor *editor = static_cast<Editor *>(aeditor);
446 	editor->label->setText(GetDisplayString(val, nullptr, false));
447 	editor->last_value = val;
448 	editor->property_path = property_path;
449 	if (editor->button_pending) emit editor->button->pressed();
450 }
451 
CreateEditor(const class C4PropertyDelegateFactory * parent_delegate,QWidget * parent,const QStyleOptionViewItem & option,bool by_selection,bool is_child) const452 QWidget *C4PropertyDelegateDescendPath::CreateEditor(const class C4PropertyDelegateFactory *parent_delegate, QWidget *parent, const QStyleOptionViewItem &option, bool by_selection, bool is_child) const
453 {
454 	// Otherwise create display and button to descend path
455 	Editor *editor;
456 	std::unique_ptr<Editor> peditor((editor = new Editor(parent)));
457 	connect(editor->button, &QPushButton::pressed, this, [editor, this]() {
458 		// Value to descend into: Use last value on auto-press because it will not have been updated into the game yet
459 		// (and cannot be without going async in network mode)
460 		// On regular button press, re-resolve path to value
461 		C4Value val = editor->button_pending ? editor->last_value : editor->property_path.ResolveValue();
462 		bool is_proplist = !!val.getPropList(), is_array = !!val.getArray();
463 		if (is_proplist || is_array)
464 		{
465 			C4PropList *info_proplist = this->info_proplist.getPropList();
466 			// Allow descending into a sub-path
467 			C4PropertyPath descend_property_path(editor->property_path);
468 			if (is_proplist && descend_path)
469 			{
470 				// Descend value into sub-path
471 				val._getPropList()->GetPropertyByS(descend_path.Get(), &val);
472 				// Descend info_proplist into sub-path
473 				if (info_proplist)
474 				{
475 					C4PropList *info_editorprops = info_proplist->GetPropertyPropList(P_EditorProps);
476 					if (info_editorprops)
477 					{
478 						C4Value sub_info_proplist_val;
479 						info_editorprops->GetPropertyByS(descend_path.Get(), &sub_info_proplist_val);
480 						info_proplist = sub_info_proplist_val.getPropList();
481 					}
482 				}
483 				// Descend property path into sub-path
484 				descend_property_path = C4PropertyPath(descend_property_path, descend_path->GetCStr());
485 			}
486 			// No info proplist: Fall back to regular proplist viewing mode
487 			if (!info_proplist) info_proplist = val.getPropList();
488 			this->factory->GetPropertyModel()->DescendPath(val, info_proplist, descend_property_path);
489 			::Console.EditCursor.InvalidateSelection();
490 		}
491 	});
492 	if (by_selection && edit_on_selection) editor->button_pending = true;
493 	return peditor.release();
494 }
495 
496 
497 /* Array descend delegate */
498 
C4PropertyDelegateArray(const class C4PropertyDelegateFactory * factory,C4PropList * props)499 C4PropertyDelegateArray::C4PropertyDelegateArray(const class C4PropertyDelegateFactory *factory, C4PropList *props)
500 	: C4PropertyDelegateDescendPath(factory, props), max_array_display(0), element_delegate(nullptr)
501 {
502 	if (props)
503 	{
504 		max_array_display = props->GetPropertyInt(P_Display);
505 	}
506 }
507 
ResolveElementDelegate() const508 void C4PropertyDelegateArray::ResolveElementDelegate() const
509 {
510 	if (!element_delegate)
511 	{
512 		C4Value element_delegate_value;
513 		C4PropList *info_proplist = this->info_proplist.getPropList();
514 		if (info_proplist) info_proplist->GetProperty(P_Elements, &element_delegate_value);
515 		element_delegate = factory->GetDelegateByValue(element_delegate_value);
516 	}
517 }
518 
GetDisplayString(const C4Value & v,C4Object * obj,bool short_names) const519 QString C4PropertyDelegateArray::GetDisplayString(const C4Value &v, C4Object *obj, bool short_names) const
520 {
521 	C4ValueArray *arr = v.getArray();
522 	if (!arr) return QString(LoadResStr("IDS_CNS_INVALID"));
523 	int32_t n = v._getArray()->GetSize();
524 	ResolveElementDelegate();
525 	if (max_array_display && n)
526 	{
527 		QString result = "[";
528 		for (int32_t i = 0; i < std::min<int32_t>(n, max_array_display); ++i)
529 		{
530 			if (i) result += ",";
531 			result += element_delegate->GetDisplayString(v._getArray()->GetItem(i), obj, true);
532 		}
533 		if (n > max_array_display) result += ",...";
534 		result += "]";
535 		return result;
536 	}
537 	else if (n || !short_names)
538 	{
539 		// Default display (or display with 0 elements): Just show element number
540 		return QString(LoadResStr("IDS_CNS_ARRAYSHORT")).arg(n);
541 	}
542 	else
543 	{
544 		// Short display of empty array: Just leave it out.
545 		return QString("");
546 	}
547 }
548 
IsPasteValid(const C4Value & val) const549 bool C4PropertyDelegateArray::IsPasteValid(const C4Value &val) const
550 {
551 	// Check array type and all contents
552 	C4ValueArray *arr = val.getArray();
553 	if (!arr) return false;
554 	int32_t n = arr->GetSize();
555 	if (n)
556 	{
557 		ResolveElementDelegate();
558 		for (int32_t i = 0; i < arr->GetSize(); ++i)
559 		{
560 			C4Value item = arr->GetItem(i);
561 			if (!element_delegate->IsPasteValid(item)) return false;
562 		}
563 	}
564 	return true;
565 }
566 
567 
568 /* Proplist descend delegate */
569 
C4PropertyDelegatePropList(const class C4PropertyDelegateFactory * factory,C4PropList * props)570 C4PropertyDelegatePropList::C4PropertyDelegatePropList(const class C4PropertyDelegateFactory *factory, C4PropList *props)
571 	: C4PropertyDelegateDescendPath(factory, props)
572 {
573 	if (props)
574 	{
575 		display_string = props->GetPropertyStr(P_Display);
576 	}
577 }
578 
GetDisplayString(const C4Value & v,C4Object * obj,bool short_names) const579 QString C4PropertyDelegatePropList::GetDisplayString(const C4Value &v, C4Object *obj, bool short_names) const
580 {
581 	C4PropList *data = v.getPropList();
582 	if (!data) return QString(LoadResStr("IDS_CNS_INVALID"));
583 	if (!display_string) return QString("{...}");
584 	C4PropList *info_proplist = this->info_proplist.getPropList();
585 	C4PropList *info_editorprops = info_proplist ? info_proplist->GetPropertyPropList(P_EditorProps) : nullptr;
586 	// Replace all {{name}} by property values of name
587 	QString result = display_string->GetCStr();
588 	int32_t pos0, pos1;
589 	C4Value cv;
590 	while ((pos0 = result.indexOf("{{")) >= 0)
591 	{
592 		pos1 = result.indexOf("}}", pos0+2);
593 		if (pos1 < 0) break; // placeholder not closed
594 		// Get child value
595 		QString substring = result.mid(pos0+2, pos1-pos0-2);
596 		C4RefCntPointer<C4String> psubstring = ::Strings.RegString(substring.toUtf8());
597 		if (!data->GetPropertyByS(psubstring.Get(), &cv)) cv.Set0();
598 		// Try to display using child delegate
599 		QString display_value;
600 		if (info_editorprops)
601 		{
602 			C4Value child_delegate_val;
603 			if (info_editorprops->GetPropertyByS(psubstring.Get(), &child_delegate_val))
604 			{
605 				C4PropertyDelegate *child_delegate = factory->GetDelegateByValue(child_delegate_val);
606 				if (child_delegate)
607 				{
608 					display_value = child_delegate->GetDisplayString(cv, obj, true);
609 				}
610 			}
611 		}
612 		// If there is no child delegate, fall back to GetDataString()
613 		if (display_value.isEmpty()) display_value = cv.GetDataString().getData();
614 		// Put value into display string
615 		result.replace(pos0, pos1 - pos0 + 2, display_value);
616 	}
617 	return result;
618 }
619 
IsPasteValid(const C4Value & val) const620 bool C4PropertyDelegatePropList::IsPasteValid(const C4Value &val) const
621 {
622 	// Check proplist type
623 	C4PropList *pval = val.getPropList();
624 	if (!pval) return false;
625 	// Are there restrictions on allowed properties?
626 	C4PropList *info_proplist = this->info_proplist.getPropList();
627 	C4PropList *info_editorprops = info_proplist ? info_proplist->GetPropertyPropList(P_EditorProps) : nullptr;
628 	if (!info_editorprops) return true; // No restrictions: Allow everything
629 	// Otherwise all types properties must be valid for paste
630 	// (Extra properties are OK)
631 	std::vector< C4String * > properties = info_editorprops->GetUnsortedProperties(nullptr, nullptr);
632 	for (C4String *prop_name : properties)
633 	{
634 		if (prop_name == &::Strings.P[P_Prototype]) continue;
635 		C4Value child_delegate_val;
636 		if (!info_editorprops->GetPropertyByS(prop_name, &child_delegate_val)) continue;
637 		C4PropertyDelegate *child_delegate = factory->GetDelegateByValue(child_delegate_val);
638 		if (!child_delegate) continue;
639 		C4Value child_val;
640 		pval->GetPropertyByS(prop_name, &child_val);
641 		if (!child_delegate->IsPasteValid(child_val)) return false;
642 	}
643 	return true;
644 }
645 
646 
647 /* Effect delegate: Allows removal and descend into proplist */
648 
C4PropertyDelegateEffectEditor(QWidget * parent)649 C4PropertyDelegateEffectEditor::C4PropertyDelegateEffectEditor(QWidget *parent) : QWidget(parent), layout(nullptr), remove_button(nullptr), edit_button(nullptr)
650 {
651 	layout = new QHBoxLayout(this);
652 	layout->setContentsMargins(0, 0, 0, 0);
653 	layout->setMargin(0);
654 	layout->setSpacing(0);
655 	remove_button = new QPushButton(QString(LoadResStr("IDS_CNS_REMOVE")), this);
656 	layout->addWidget(remove_button);
657 	edit_button = new QPushButton(QString(LoadResStr("IDS_CNS_MORE")), this);
658 	layout->addWidget(edit_button);
659 	// Make sure to draw over view in background
660 	setAutoFillBackground(true);
661 }
662 
C4PropertyDelegateEffect(const class C4PropertyDelegateFactory * factory,C4PropList * props)663 C4PropertyDelegateEffect::C4PropertyDelegateEffect(const class C4PropertyDelegateFactory *factory, C4PropList *props)
664 	: C4PropertyDelegate(factory, props)
665 {
666 }
667 
SetEditorData(QWidget * aeditor,const C4Value & val,const C4PropertyPath & property_path) const668 void C4PropertyDelegateEffect::SetEditorData(QWidget *aeditor, const C4Value &val, const C4PropertyPath &property_path) const
669 {
670 	Editor *editor = static_cast<Editor *>(aeditor);
671 	editor->property_path = property_path;
672 }
673 
CreateEditor(const class C4PropertyDelegateFactory * parent_delegate,QWidget * parent,const QStyleOptionViewItem & option,bool by_selection,bool is_child) const674 QWidget *C4PropertyDelegateEffect::CreateEditor(const class C4PropertyDelegateFactory *parent_delegate, QWidget *parent, const QStyleOptionViewItem &option, bool by_selection, bool is_child) const
675 {
676 	Editor *editor;
677 	std::unique_ptr<Editor> peditor((editor = new Editor(parent)));
678 	// Remove effect button
679 	connect(editor->remove_button, &QPushButton::pressed, this, [editor, this]() {
680 		// Compose an effect remove call
681 		editor->property_path.DoCall("RemoveEffect(nil, nil, %s)");
682 		emit EditingDoneSignal(editor);
683 	});
684 	// Edit effect button
685 	connect(editor->edit_button, &QPushButton::pressed, this, [editor, this]() {
686 		// Descend into effect proplist (if the effect still exists)
687 		C4Value effect_val = editor->property_path.ResolveValue();
688 		C4PropList *effect_proplist = effect_val.getPropList();
689 		if (!effect_proplist)
690 		{
691 			// Effect lost
692 			emit EditingDoneSignal(editor);
693 		}
694 		else
695 		{
696 			// Effect OK. Edit it.
697 			this->factory->GetPropertyModel()->DescendPath(effect_val, effect_proplist, editor->property_path);
698 			::Console.EditCursor.InvalidateSelection();
699 		}
700 	});
701 	return peditor.release();
702 }
703 
GetDisplayString(const C4Value & v,C4Object * obj,bool short_names) const704 QString C4PropertyDelegateEffect::GetDisplayString(const C4Value &v, C4Object *obj, bool short_names) const
705 {
706 	C4PropList *effect_proplist = v.getPropList();
707 	C4Effect *effect = effect_proplist ? effect_proplist->GetEffect() : nullptr;
708 	if (effect)
709 	{
710 		if (effect->IsActive())
711 		{
712 			return QString("t=%1, interval=%2").arg(effect->iTime).arg(effect->iInterval);
713 		}
714 		else
715 		{
716 			return QString(LoadResStr("IDS_CNS_DEADEFFECT"));
717 		}
718 	}
719 	else
720 	{
721 		return QString("nil");
722 	}
723 }
724 
GetPropertyValue(const C4Value & container,C4String * key,int32_t index,C4Value * out_val) const725 bool C4PropertyDelegateEffect::GetPropertyValue(const C4Value &container, C4String *key, int32_t index, C4Value *out_val) const
726 {
727 	// Resolve effect by calling script function
728 	if (!key) return false;
729 	*out_val = AulExec.DirectExec(::ScriptEngine.GetPropList(), key->GetCStr(), "resolve effect", false, nullptr);
730 	return true;
731 }
732 
GetPathForProperty(C4ConsoleQtPropListModelProperty * editor_prop) const733 C4PropertyPath C4PropertyDelegateEffect::GetPathForProperty(C4ConsoleQtPropListModelProperty *editor_prop) const
734 {
735 	// Property path is used directly for getting effect. No set function needed.
736 	return editor_prop->property_path;
737 }
738 
739 
740 /* Color delegate */
741 
C4PropertyDelegateColor(const class C4PropertyDelegateFactory * factory,C4PropList * props)742 C4PropertyDelegateColor::C4PropertyDelegateColor(const class C4PropertyDelegateFactory *factory, C4PropList *props)
743 	: C4PropertyDelegate(factory, props), alpha_mask(0u)
744 {
745 	if (props)
746 	{
747 		alpha_mask = props->GetPropertyInt(P_Alpha) << 24;
748 	}
749 }
750 
GetTextColorForBackground(uint32_t background_color)751 uint32_t GetTextColorForBackground(uint32_t background_color)
752 {
753 	// White text on dark background; black text on bright background
754 	uint8_t r = (background_color >> 16) & 0xff;
755 	uint8_t g = (background_color >> 8) & 0xff;
756 	uint8_t b = (background_color >> 0) & 0xff;
757 	int32_t lgt = r * 30 + g * 59 + b * 11;
758 	return (lgt > 16000) ? 0 : 0xffffff;
759 }
760 
SetEditorData(QWidget * aeditor,const C4Value & val,const C4PropertyPath & property_path) const761 void C4PropertyDelegateColor::SetEditorData(QWidget *aeditor, const C4Value &val, const C4PropertyPath &property_path) const
762 {
763 	Editor *editor = static_cast<Editor *>(aeditor);
764 	uint32_t background_color = static_cast<uint32_t>(val.getInt()) & 0xffffff;
765 	uint32_t foreground_color = GetTextColorForBackground(background_color);
766 	QPalette palette = editor->label->palette();
767 	palette.setColor(editor->label->backgroundRole(), QColor(QRgb(background_color)));
768 	palette.setColor(editor->label->foregroundRole(), QColor(QRgb(foreground_color)));
769 	editor->label->setPalette(palette);
770 	editor->label->setAutoFillBackground(true);
771 	editor->label->setText(GetDisplayString(val, nullptr, false));
772 	editor->last_value = val;
773 }
774 
SetModelData(QObject * aeditor,const C4PropertyPath & property_path,C4ConsoleQtShape * prop_shape) const775 void C4PropertyDelegateColor::SetModelData(QObject *aeditor, const C4PropertyPath &property_path, C4ConsoleQtShape *prop_shape) const
776 {
777 	Editor *editor = static_cast<Editor *>(aeditor);
778 	property_path.SetProperty(editor->last_value);
779 	factory->GetPropertyModel()->DoOnUpdateCall(property_path, this);
780 }
781 
CreateEditor(const class C4PropertyDelegateFactory * parent_delegate,QWidget * parent,const QStyleOptionViewItem & option,bool by_selection,bool is_child) const782 QWidget *C4PropertyDelegateColor::CreateEditor(const class C4PropertyDelegateFactory *parent_delegate, QWidget *parent, const QStyleOptionViewItem &option, bool by_selection, bool is_child) const
783 {
784 	Editor *editor;
785 	std::unique_ptr<Editor> peditor((editor = new Editor(parent)));
786 	connect(editor->button, &QPushButton::pressed, this, [editor, this]() {
787 		this->OpenColorDialogue(editor);
788 	});
789 	// Selection in child enum: Open dialogue immediately
790 	if (by_selection && is_child) OpenColorDialogue(editor);
791 	return peditor.release();
792 }
793 
GetDisplayString(const C4Value & v,C4Object * obj,bool short_names) const794 QString C4PropertyDelegateColor::GetDisplayString(const C4Value &v, C4Object *obj, bool short_names) const
795 {
796 	return QString("#%1").arg(uint32_t(v.getInt()), 8, 16, QChar('0'));
797 }
798 
GetDisplayTextColor(const C4Value & val,class C4Object * obj) const799 QColor C4PropertyDelegateColor::GetDisplayTextColor(const C4Value &val, class C4Object *obj) const
800 {
801 	uint32_t background_color = static_cast<uint32_t>(val.getInt()) & 0xffffff;
802 	uint32_t foreground_color = GetTextColorForBackground(background_color);
803 	return QColor(foreground_color);
804 }
805 
GetDisplayBackgroundColor(const C4Value & val,class C4Object * obj) const806 QColor C4PropertyDelegateColor::GetDisplayBackgroundColor(const C4Value &val, class C4Object *obj) const
807 {
808 	return static_cast<uint32_t>(val.getInt()) & 0xffffff;
809 }
810 
IsPasteValid(const C4Value & val) const811 bool C4PropertyDelegateColor::IsPasteValid(const C4Value &val) const
812 {
813 	// Color is always int
814 	if (val.GetType() != C4V_Int) return false;
815 	return true;
816 }
817 
OpenColorDialogue(C4PropertyDelegateLabelAndButtonWidget * editor) const818 void C4PropertyDelegateColor::OpenColorDialogue(C4PropertyDelegateLabelAndButtonWidget *editor) const
819 {
820 	// Show actual dialogue to change the color
821 	QColor clr = QColorDialog::getColor(QColor(editor->last_value.getInt() & (~alpha_mask)), editor, QString(), QColorDialog::ShowAlphaChannel);
822 	editor->last_value.SetInt(clr.rgba() | alpha_mask);
823 	this->SetEditorData(editor, editor->last_value, C4PropertyPath()); // force update on display
824 	emit EditingDoneSignal(editor);
825 }
826 
827 
828 /* Enum delegate combo box item delegate */
829 
editorEvent(QEvent * event,QAbstractItemModel * model,const QStyleOptionViewItem & option,const QModelIndex & index)830 bool C4StyledItemDelegateWithButton::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index)
831 {
832 	// Mouse move over a cell: Display tooltip if over help button
833 	QEvent::Type trigger_type = (button_type == BT_Help) ? QEvent::MouseMove : QEvent::MouseButtonPress;
834 	if (event->type() == trigger_type)
835 	{
836 		QVariant btn = model->data(index, Qt::DecorationRole);
837 		if (!btn.isNull())
838 		{
839 			QMouseEvent *mevent = static_cast<QMouseEvent *>(event);
840 			if (option.rect.contains(mevent->localPos().toPoint()))
841 			{
842 				if (mevent->localPos().x() >= option.rect.x() + option.rect.width() - option.rect.height())
843 				{
844 					switch (button_type)
845 					{
846 					case BT_Help:
847 						if (Config.Developer.ShowHelp)
848 						{
849 							QString tooltip_text = model->data(index, Qt::ToolTipRole).toString();
850 							QToolTip::showText(mevent->globalPos(), tooltip_text);
851 						}
852 						break;
853 					case BT_PlaySound:
854 						StartSoundEffect(model->data(index, Qt::ToolTipRole).toString().toUtf8());
855 						return true; // handled
856 					}
857 				}
858 			}
859 		}
860 	}
861 	return QStyledItemDelegate::editorEvent(event, model, option, index);
862 }
863 
paint(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const864 void C4StyledItemDelegateWithButton::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
865 {
866 	// Paint icon on the right
867 	QStyleOptionViewItem override_option = option;
868 	override_option.decorationPosition = QStyleOptionViewItem::Right;
869 	QStyledItemDelegate::paint(painter, override_option, index);
870 }
871 
872 
873 
874 /* Enum delegate combo box */
875 
C4DeepQComboBox(QWidget * parent,C4StyledItemDelegateWithButton::ButtonType button_type,bool editable)876 C4DeepQComboBox::C4DeepQComboBox(QWidget *parent, C4StyledItemDelegateWithButton::ButtonType button_type, bool editable)
877 	: QComboBox(parent), last_popup_height(0), is_next_close_blocked(false), editable(editable), manual_text_edited(false)
878 {
879 	item_delegate = std::make_unique<C4StyledItemDelegateWithButton>(button_type);
880 	QTreeView *view = new QTreeView(this);
881 	view->setFrameShape(QFrame::NoFrame);
882 	view->setSelectionBehavior(QTreeView::SelectRows);
883 	view->setAllColumnsShowFocus(true);
884 	view->header()->hide();
885 	view->setItemDelegate(item_delegate.get());
886 	setEditable(editable);
887 	// On expansion, enlarge view if necessery
888 	connect(view, &QTreeView::expanded, this, [this, view](const QModelIndex &index)
889 	{
890 		if (this->model() && view->parentWidget())
891 		{
892 			int child_row_count = this->model()->rowCount(index);
893 			if (child_row_count > 0)
894 			{
895 				// Get space to contain expanded leaf+1 item
896 				QModelIndex last_index = this->model()->index(child_row_count - 1, 0, index);
897 				int needed_height = view->visualRect(last_index).bottom() - view->visualRect(index).top() + view->height() - view->parentWidget()->height() + view->visualRect(last_index).height();
898 				int available_height = QApplication::desktop()->availableGeometry(view->mapToGlobal(QPoint(1, 1))).height(); // but do not expand past screen size
899 				int new_height = std::min(needed_height, available_height - 20);
900 				if (view->parentWidget()->height() < new_height) view->parentWidget()->resize(view->parentWidget()->width(), (this->last_popup_height=new_height));
901 			}
902 		}
903 	});
904 	// On selection, highlight object in editor
905 	view->setMouseTracking(true);
906 	connect(view, &QTreeView::entered, this, [this](const QModelIndex &index)
907 	{
908 		C4Object *obj = nullptr;
909 		int32_t obj_number = this->model()->data(index, ObjectHighlightRole).toInt();
910 		if (obj_number) obj = ::Objects.SafeObjectPointer(obj_number);
911 		::Console.EditCursor.SetHighlightedObject(obj);
912 	});
913 	// New item selection through combo box: Update model position
914 	connect(this, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated),
915 		[this](int index)
916 	{
917 		QModelIndex current = this->view()->currentIndex();
918 		QVariant selected_data = this->model()->data(current, OptionIndexRole);
919 		if (selected_data.type() == QVariant::Int)
920 		{
921 			// Reset manual text edit flag because the text is now provided by the view
922 			manual_text_edited = false;
923 			// Finish selection
924 			setCurrentModelIndex(current);
925 			emit NewItemSelected(selected_data.toInt());
926 		}
927 	});
928 	// New text typed in
929 	if (editable)
930 	{
931 		// text change event only sent after manual text change
932 		connect(lineEdit(), &QLineEdit::textEdited, [this](const QString &text)
933 		{
934 			manual_text_edited = true;
935 			last_edited_text = text;
936 		});
937 		// reflect in data after return press and when focus is lost
938 		connect(lineEdit(), &QLineEdit::returnPressed, [this]()
939 		{
940 			if (manual_text_edited)
941 			{
942 				emit TextChanged(last_edited_text);
943 				manual_text_edited = false;
944 			}
945 		});
946 		connect(lineEdit(), &QLineEdit::editingFinished, [this]()
947 		{
948 			if (manual_text_edited)
949 			{
950 				emit TextChanged(last_edited_text);
951 				manual_text_edited = false;
952 			}
953 		});
954 	}
955 	// Connect view to combobox
956 	setView(view);
957 	view->viewport()->installEventFilter(this);
958 	// No help icons in main box, unless dropped down
959 	default_icon_size = iconSize();
960 	setIconSize(QSize(0, 0));
961 }
962 
showPopup()963 void C4DeepQComboBox::showPopup()
964 {
965 	// New selection: Reset to root of model
966 	setRootModelIndex(QModelIndex());
967 	setIconSize(default_icon_size);
968 	QComboBox::showPopup();
969 	view()->setMinimumWidth(200); // prevent element list from becoming too small in nested dialogues
970 	if (last_popup_height && view()->parentWidget()) view()->parentWidget()->resize(view()->parentWidget()->width(), last_popup_height);
971 }
972 
hidePopup()973 void C4DeepQComboBox::hidePopup()
974 {
975 	// Cleanup tree combobox
976 	::Console.EditCursor.SetHighlightedObject(nullptr);
977 	setIconSize(QSize(0, 0));
978 	QComboBox::hidePopup();
979 }
980 
setCurrentModelIndex(QModelIndex new_index)981 void C4DeepQComboBox::setCurrentModelIndex(QModelIndex new_index)
982 {
983 	setRootModelIndex(new_index.parent());
984 	setCurrentIndex(new_index.row());
985 	// Adjust text
986 	if (editable)
987 	{
988 		lineEdit()->setText(this->model()->data(new_index, ValueStringRole).toString());
989 		manual_text_edited = false;
990 	}
991 }
992 
GetCurrentSelectionIndex()993 int32_t C4DeepQComboBox::GetCurrentSelectionIndex()
994 {
995 	QVariant selected_data = model()->data(model()->index(currentIndex(), 0, rootModelIndex()), OptionIndexRole);
996 	if (selected_data.type() == QVariant::Int)
997 	{
998 		// Valid selection
999 		return selected_data.toInt();
1000 	}
1001 	else
1002 	{
1003 		// Invalid selection
1004 		return -1;
1005 	}
1006 }
1007 
1008 // event filter for view: Catch mouse clicks to prevent closing from simple mouse clicks
eventFilter(QObject * obj,QEvent * event)1009 bool C4DeepQComboBox::eventFilter(QObject *obj, QEvent *event)
1010 {
1011 	if (obj == view()->viewport())
1012 	{
1013 		if (event->type() == QEvent::MouseButtonPress)
1014 		{
1015 			QPoint pos = static_cast<QMouseEvent *>(event)->pos();
1016 			QModelIndex pressed_index = view()->indexAt(pos);
1017 			QRect item_rect = view()->visualRect(pressed_index);
1018 			// Check if a group was clicked
1019 			bool item_clicked = item_rect.contains(pos);
1020 			if (item_clicked)
1021 			{
1022 				QVariant selected_data = model()->data(pressed_index, OptionIndexRole);
1023 				if (selected_data.type() != QVariant::Int)
1024 				{
1025 					// This is a group. Just expand that entry.
1026 					QTreeView *tview = static_cast<QTreeView *>(view());
1027 					if (!tview->isExpanded(pressed_index))
1028 					{
1029 						tview->setExpanded(pressed_index, true);
1030 						int32_t child_row_count = model()->rowCount(pressed_index);
1031 						tview->scrollTo(model()->index(child_row_count - 1, 0, pressed_index), QAbstractItemView::EnsureVisible);
1032 						tview->scrollTo(pressed_index, QAbstractItemView::EnsureVisible);
1033 					}
1034 					is_next_close_blocked = true;
1035 					return true;
1036 				}
1037 			}
1038 			else
1039 			{
1040 				is_next_close_blocked = true;
1041 				return false;
1042 			}
1043 			// Delegate handling: The forward to delegate screws up for me sometimes and just stops randomly
1044 			// Prevent this by calling the event directly
1045 			QStyleOptionViewItem option;
1046 			option.rect = view()->visualRect(pressed_index);
1047 			if (item_delegate->editorEvent(event, model(), option, pressed_index))
1048 			{
1049 				// If the down event is taken by a music play event, ignore the following button up
1050 				is_next_close_blocked = true;
1051 				return true;
1052 			}
1053 		}
1054 		else if (event->type() == QEvent::MouseButtonRelease)
1055 		{
1056 			if (is_next_close_blocked)
1057 			{
1058 				is_next_close_blocked = false;
1059 				return true;
1060 			}
1061 		}
1062 	}
1063 	return QComboBox::eventFilter(obj, event);
1064 }
1065 
1066 
1067 /* Enumeration delegate editor */
1068 
paintEvent(QPaintEvent * ev)1069 void C4PropertyDelegateEnumEditor::paintEvent(QPaintEvent *ev)
1070 {
1071 	// Draw self
1072 	QWidget::paintEvent(ev);
1073 	// Draw shape widget
1074 	if (paint_parameter_delegate && parameter_widget)
1075 	{
1076 		QPainter p(this);
1077 		QStyleOptionViewItem view_item;
1078 		view_item.rect.setTopLeft(parameter_widget->mapToParent(parameter_widget->rect().topLeft()));
1079 		view_item.rect.setBottomRight(parameter_widget->mapToParent(parameter_widget->rect().bottomRight()));
1080 		paint_parameter_delegate->Paint(&p, view_item, last_parameter_val);
1081 		//p.fillRect(view_item.rect, QColor("red"));
1082 	}
1083 }
1084 
1085 
1086 /* Enumeration (dropdown list) delegate */
1087 
C4PropertyDelegateEnum(const C4PropertyDelegateFactory * factory,C4PropList * props,const C4ValueArray * poptions)1088 C4PropertyDelegateEnum::C4PropertyDelegateEnum(const C4PropertyDelegateFactory *factory, C4PropList *props, const C4ValueArray *poptions)
1089 	: C4PropertyDelegate(factory, props), allow_editing(false), sorted(false)
1090 {
1091 	// Build enum options from C4Value definitions in script
1092 	if (!poptions && props) poptions = props->GetPropertyArray(P_Options);
1093 	C4String *default_option_key, *default_value_key = nullptr;
1094 	if (props)
1095 	{
1096 		default_option_key = props->GetPropertyStr(P_OptionKey);
1097 		default_value_key = props->GetPropertyStr(P_ValueKey);
1098 		allow_editing = props->GetPropertyBool(P_AllowEditing);
1099 		empty_name = props->GetPropertyStr(P_EmptyName);
1100 		sorted = props->GetPropertyBool(P_Sorted);
1101 		default_option.option_key = default_option_key;
1102 		default_option.value_key = default_value_key;
1103 	}
1104 	if (poptions)
1105 	{
1106 		options.reserve(poptions->GetSize());
1107 		for (int32_t i = 0; i < poptions->GetSize(); ++i)
1108 		{
1109 			const C4Value &v = poptions->GetItem(i);
1110 			C4PropList *props = v.getPropList();
1111 			if (!props) continue;
1112 			Option option;
1113 			option.props.SetPropList(props);
1114 			option.name = props->GetPropertyStr(P_Name);
1115 			if (!option.name) option.name = ::Strings.RegString("???");
1116 			option.help = props->GetPropertyStr(P_EditorHelp);
1117 			option.group = props->GetPropertyStr(P_Group);
1118 			option.value_key = props->GetPropertyStr(P_ValueKey);
1119 			if (!option.value_key) option.value_key = default_value_key;
1120 			props->GetProperty(P_Value, &option.value);
1121 			if (option.value.GetType() == C4V_Nil && empty_name) option.name = empty_name.Get();
1122 			option.short_name = props->GetPropertyStr(P_ShortName);
1123 			if (!option.short_name) option.short_name = option.name.Get();
1124 			props->GetProperty(P_DefaultValueFunction, &option.value_function);
1125 			option.type = C4V_Type(props->GetPropertyInt(P_Type, C4V_Any));
1126 			option.option_key = props->GetPropertyStr(P_OptionKey);
1127 			if (!option.option_key) option.option_key = default_option_key;
1128 			// Derive storage type from given elements in delegate definition
1129 			if (option.type != C4V_Any)
1130 				option.storage_type = Option::StorageByType;
1131 			else if (option.option_key && option.value.GetType() != C4V_Nil)
1132 				option.storage_type = Option::StorageByKey;
1133 			else
1134 				option.storage_type = Option::StorageByValue;
1135 			// Child delegate for value (resolved at runtime because there may be circular references)
1136 			props->GetProperty(P_Delegate, &option.adelegate_val);
1137 			option.priority = props->GetPropertyInt(P_Priority);
1138 			option.force_serialization = props->GetPropertyInt(P_ForceSerialization);
1139 			options.push_back(option);
1140 		}
1141 	}
1142 }
1143 
CreateOptionModel() const1144 QStandardItemModel *C4PropertyDelegateEnum::CreateOptionModel() const
1145 {
1146 	// Create a QStandardItemModel tree from all options and their groups
1147 	std::unique_ptr<QStandardItemModel> model(new QStandardItemModel());
1148 	model->setColumnCount(1);
1149 	int idx = 0;
1150 	for (const Option &opt : options)
1151 	{
1152 		QStandardItem *new_item = model->invisibleRootItem(), *parent = nullptr;
1153 		QStringList group_names;
1154 		if (opt.group) group_names.append(QString(opt.group->GetCStr()).split(QString("/")));
1155 		group_names.append(opt.name->GetCStr());
1156 		for (const QString &group_name : group_names)
1157 		{
1158 			parent = new_item;
1159 			int row_index = -1;
1160 			for (int check_row_index = 0; check_row_index < new_item->rowCount(); ++check_row_index)
1161 				if (new_item->child(check_row_index, 0)->text() == group_name)
1162 				{
1163 					row_index = check_row_index;
1164 					new_item = new_item->child(check_row_index, 0);
1165 					break;
1166 				}
1167 			if (row_index < 0)
1168 			{
1169 				QStandardItem *new_group = new QStandardItem(group_name);
1170 				if (sorted)
1171 				{
1172 					// Groups always sorted by name. Could also sort by priority of highest priority element?
1173 					new_group->setData("010000000"+group_name, C4DeepQComboBox::PriorityNameSortRole);
1174 				}
1175 				new_item->appendRow(new_group);
1176 				new_item = new_group;
1177 			}
1178 		}
1179 		// If this item is already set, add a duplicate entry
1180 		if (new_item->data(C4DeepQComboBox::OptionIndexRole).isValid())
1181 		{
1182 			new_item = new QStandardItem(QString(opt.name->GetCStr()));
1183 			parent->appendRow(new_item);
1184 		}
1185 		// Sort key
1186 		if (sorted)
1187 		{
1188 			// Reverse priority and make positive, so we can sort by descending priority but ascending name
1189 			new_item->setData(QString(FormatString("%09d%s", (int)(10000000-opt.priority), opt.name->GetCStr()).getData()), C4DeepQComboBox::PriorityNameSortRole);
1190 		}
1191 		new_item->setData(QVariant(idx), C4DeepQComboBox::OptionIndexRole);
1192 		C4Object *item_obj_data = opt.value.getObj();
1193 		if (item_obj_data) new_item->setData(QVariant(item_obj_data->Number), C4DeepQComboBox::ObjectHighlightRole);
1194 		QString help = QString((opt.help ? opt.help : opt.name)->GetCStr());
1195 		new_item->setData(help.replace('|', '\n'), Qt::ToolTipRole);
1196 		if (opt.help) new_item->setData(QIcon(":/editor/res/Help.png"), Qt::DecorationRole);
1197 		if (opt.sound_name) new_item->setData(QIcon(":/editor/res/Sound.png"), Qt::DecorationRole);
1198 		if (allow_editing)
1199 		{
1200 			C4String *s = opt.value.getStr();
1201 			new_item->setData(QString(s ? s->GetCStr() : ""), C4DeepQComboBox::ValueStringRole);
1202 		}
1203 		++idx;
1204 	}
1205 	// Sort model and all groups
1206 	if (sorted)
1207 	{
1208 		model->setSortRole(C4DeepQComboBox::PriorityNameSortRole);
1209 		model->sort(0, Qt::AscendingOrder);
1210 	}
1211 	return model.release();
1212 }
1213 
ClearOptions()1214 void C4PropertyDelegateEnum::ClearOptions()
1215 {
1216 	options.clear();
1217 }
1218 
ReserveOptions(int32_t num)1219 void C4PropertyDelegateEnum::ReserveOptions(int32_t num)
1220 {
1221 	options.reserve(num);
1222 }
1223 
AddTypeOption(C4String * name,C4V_Type type,const C4Value & val,C4PropertyDelegate * adelegate)1224 void C4PropertyDelegateEnum::AddTypeOption(C4String *name, C4V_Type type, const C4Value &val, C4PropertyDelegate *adelegate)
1225 {
1226 	Option option;
1227 	option.name = name;
1228 	option.short_name = name;
1229 	option.type = type;
1230 	option.value = val;
1231 	option.storage_type = Option::StorageByType;
1232 	option.adelegate = adelegate;
1233 	options.push_back(option);
1234 }
1235 
AddConstOption(C4String * name,const C4Value & val,C4String * group,C4String * sound_name)1236 void C4PropertyDelegateEnum::AddConstOption(C4String *name, const C4Value &val, C4String *group, C4String *sound_name)
1237 {
1238 	Option option;
1239 	option.name = name;
1240 	option.short_name = name;
1241 	option.group = group;
1242 	option.value = val;
1243 	option.storage_type = Option::StorageByValue;
1244 	if (sound_name)
1245 	{
1246 		option.sound_name = sound_name;
1247 		option.help = sound_name;
1248 	}
1249 	options.push_back(option);
1250 }
1251 
GetOptionByValue(const C4Value & val) const1252 int32_t C4PropertyDelegateEnum::GetOptionByValue(const C4Value &val) const
1253 {
1254 	int32_t iopt = 0;
1255 	bool match = false;
1256 	for (auto &option : options)
1257 	{
1258 		switch (option.storage_type)
1259 		{
1260 		case Option::StorageByType:
1261 			match = (val.GetTypeEx() == option.type);
1262 			break;
1263 		case Option::StorageByValue:
1264 			match = (val == option.value);
1265 			break;
1266 		case Option::StorageByKey: // Compare value to value in property. Assume undefined as nil.
1267 		{
1268 			C4PropList *props = val.getPropList();
1269 			C4PropList *def_props = option.value.getPropList();
1270 			if (props && def_props)
1271 			{
1272 				C4Value propval, defval;
1273 				props->GetPropertyByS(option.option_key.Get(), &propval);
1274 				def_props->GetPropertyByS(option.option_key.Get(), &defval);
1275 				match = (defval == propval);
1276 			}
1277 			break;
1278 		}
1279 		default: break;
1280 		}
1281 		if (match) break;
1282 		++iopt;
1283 	}
1284 	// If no option matches, return sentinel value
1285 	return match ? iopt : Editor::INDEX_Custom_Value;
1286 }
1287 
UpdateEditorParameter(C4PropertyDelegateEnum::Editor * editor,bool by_selection) const1288 void C4PropertyDelegateEnum::UpdateEditorParameter(C4PropertyDelegateEnum::Editor *editor, bool by_selection) const
1289 {
1290 	// Recreate parameter settings editor associated with the currently selected option of an enum
1291 	if (editor->parameter_widget)
1292 	{
1293 		editor->parameter_widget->deleteLater();
1294 		editor->parameter_widget = nullptr;
1295 	}
1296 	editor->paint_parameter_delegate = nullptr;
1297 	int32_t idx = editor->last_selection_index;
1298 	if (by_selection)
1299 	{
1300 		idx = editor->option_box->GetCurrentSelectionIndex();
1301 	}
1302 	// No parameter delegate if not a known option (custom text or invalid value)
1303 	if (idx < 0 || idx >= options.size()) return;
1304 	const Option &option = options[idx];
1305 	// Lazy-resolve parameter delegate
1306 	EnsureOptionDelegateResolved(option);
1307 	// Create editor if needed
1308 	if (option.adelegate)
1309 	{
1310 		// Determine value to be shown in editor
1311 		C4Value parameter_val;
1312 		if (!by_selection)
1313 		{
1314 			// Showing current selection: From last_val assigned in SetEditorData or by custom text
1315 			parameter_val = editor->last_val;
1316 		}
1317 		else
1318 		{
1319 			// Selecting a new item: Set the default value
1320 			parameter_val = option.value;
1321 			// Although the default value is taken directly from SetEditorData, it needs to be set here to make child access into proplists and arrays possible
1322 			// (note that actual setting is delayed by control queue and this may often the wrong value in some cases - the correct value will be shown on execution of the queue)
1323 			SetOptionValue(editor->last_get_path, option, option.value);
1324 		}
1325 		// Resolve parameter value
1326 		if (option.value_key)
1327 		{
1328 			C4Value child_val;
1329 			C4PropList *props = parameter_val.getPropList();
1330 			if (props) props->GetPropertyByS(option.value_key.Get(), &child_val);
1331 			parameter_val = child_val;
1332 		}
1333 		// Show it
1334 		editor->parameter_widget = option.adelegate->CreateEditor(factory, editor, QStyleOptionViewItem(), by_selection, true);
1335 		if (editor->parameter_widget)
1336 		{
1337 			editor->layout->addWidget(editor->parameter_widget);
1338 			C4PropertyPath delegate_value_path = editor->last_get_path;
1339 			if (option.value_key) delegate_value_path = C4PropertyPath(delegate_value_path, option.value_key->GetCStr());
1340 			option.adelegate->SetEditorData(editor->parameter_widget, parameter_val, delegate_value_path);
1341 			// Forward editing signals
1342 			connect(option.adelegate, &C4PropertyDelegate::EditorValueChangedSignal, editor->parameter_widget, [this, editor](QWidget *changed_editor)
1343 			{
1344 				if (changed_editor == editor->parameter_widget)
1345 					if (!editor->updating)
1346 						emit EditorValueChangedSignal(editor);
1347 			});
1348 			connect(option.adelegate, &C4PropertyDelegate::EditingDoneSignal, editor->parameter_widget, [this, editor](QWidget *changed_editor)
1349 			{
1350 				if (changed_editor == editor->parameter_widget) emit EditingDoneSignal(editor);
1351 			});
1352 		}
1353 		else
1354 		{
1355 			// If the parameter widget is a shape display, show a dummy widget displaying the shape instead
1356 			const C4PropertyDelegateShape *shape_delegate = option.adelegate->GetDirectShapeDelegate();
1357 			if (shape_delegate)
1358 			{
1359 				// dummy widget that is not rendered. shape rendering is forwarded through own paint function
1360 				editor->parameter_widget = new QWidget(editor);
1361 				editor->layout->addWidget(editor->parameter_widget);
1362 				editor->parameter_widget->setAttribute(Qt::WA_NoSystemBackground);
1363 				editor->parameter_widget->setAttribute(Qt::WA_TranslucentBackground);
1364 				editor->parameter_widget->setAttribute(Qt::WA_TransparentForMouseEvents);
1365 				editor->paint_parameter_delegate = shape_delegate;
1366 				editor->last_parameter_val = parameter_val;
1367 			}
1368 		}
1369 	}
1370 }
1371 
GetModelIndexByID(QStandardItemModel * model,QStandardItem * parent_item,int32_t id,const QModelIndex & parent) const1372 QModelIndex C4PropertyDelegateEnum::GetModelIndexByID(QStandardItemModel *model, QStandardItem *parent_item, int32_t id, const QModelIndex &parent) const
1373 {
1374 	// Resolve data stored in model to model index in tree
1375 	for (int row = 0; row < parent_item->rowCount(); ++row)
1376 	{
1377 		QStandardItem *child = parent_item->child(row, 0);
1378 		QVariant v = child->data(C4DeepQComboBox::OptionIndexRole);
1379 		if (v.type() == QVariant::Int && v.toInt() == id) return model->index(row, 0, parent);
1380 		if (child->rowCount())
1381 		{
1382 			QModelIndex child_match = GetModelIndexByID(model, child, id, model->index(row, 0, parent));
1383 			if (child_match.isValid()) return child_match;
1384 		}
1385 	}
1386 	return QModelIndex();
1387 }
1388 
SetEditorData(QWidget * aeditor,const C4Value & val,const C4PropertyPath & property_path) const1389 void C4PropertyDelegateEnum::SetEditorData(QWidget *aeditor, const C4Value &val, const C4PropertyPath &property_path) const
1390 {
1391 	Editor *editor = static_cast<Editor*>(aeditor);
1392 	editor->last_val = val;
1393 	editor->last_get_path = property_path;
1394 	editor->updating = true;
1395 	// Update option selection
1396 	int32_t index = GetOptionByValue(val);
1397 	if (index == Editor::INDEX_Custom_Value && !allow_editing)
1398 	{
1399 		// Invalid value and no custom values allowed? Select first item.
1400 		index = 0;
1401 	}
1402 	if (index == Editor::INDEX_Custom_Value)
1403 	{
1404 		// Custom value
1405 		C4String *val_string = val.getStr();
1406 		QString edit_string = val_string ? QString(val_string->GetCStr()) : QString(val.GetDataString().getData());
1407 		editor->option_box->setEditText(edit_string);
1408 	}
1409 	else
1410 	{
1411 		// Regular enum entry
1412 		QStandardItemModel *model = static_cast<QStandardItemModel *>(editor->option_box->model());
1413 		editor->option_box->setCurrentModelIndex(GetModelIndexByID(model, model->invisibleRootItem(), index, QModelIndex()));
1414 	}
1415 	editor->last_selection_index = index;
1416 	// Update parameter
1417 	UpdateEditorParameter(editor, false);
1418 	editor->updating = false;
1419 	// Execute pending dropdowns from creation as child enums
1420 	if (editor->dropdown_pending)
1421 	{
1422 		editor->dropdown_pending = false;
1423 		QMetaObject::invokeMethod(editor->option_box, "doShowPopup", Qt::QueuedConnection);
1424 		editor->option_box->showPopup();
1425 	}
1426 }
1427 
SetModelData(QObject * aeditor,const C4PropertyPath & property_path,C4ConsoleQtShape * prop_shape) const1428 void C4PropertyDelegateEnum::SetModelData(QObject *aeditor, const C4PropertyPath &property_path, C4ConsoleQtShape *prop_shape) const
1429 {
1430 	// Fetch value from editor
1431 	Editor *editor = static_cast<Editor*>(aeditor);
1432 	/*QStandardItemModel *model = static_cast<QStandardItemModel *>(editor->option_box->model());
1433 	QModelIndex selected_model_index = model->index(editor->option_box->currentIndex(), 0, editor->option_box->rootModelIndex());
1434 	QVariant vidx = model->data(selected_model_index, C4DeepQComboBox::OptionIndexRole);
1435 	if (vidx.type() != QVariant::Int) return;
1436 	int32_t idx = vidx.toInt();
1437 	if (idx < 0 || idx >= options.size()) return;*/
1438 	int32_t idx = editor->last_selection_index;
1439 	const Option *option;
1440 	const C4Value *option_value;
1441 	if (idx < 0)
1442 	{
1443 		option = &default_option;
1444 		option_value = &editor->last_val;
1445 	}
1446 	else
1447 	{
1448 		option = &options[std::max<int32_t>(idx, 0)];
1449 		option_value = &option->value;
1450 	}
1451 	// Store directly in value or in a proplist field?
1452 	C4PropertyPath use_path;
1453 	if (option->value_key.Get())
1454 		use_path = C4PropertyPath(property_path, option->value_key->GetCStr());
1455 	else
1456 		use_path = property_path;
1457 	// Value from a parameter or directly from the enum?
1458 	if (option->adelegate)
1459 	{
1460 		// Default value on enum change (on main path; not use_path because the default value is always given as the whole proplist)
1461 		if (editor->option_changed) SetOptionValue(property_path, *option, *option_value);
1462 		// Value from a parameter.
1463 		// Using a setter function?
1464 		use_path = option->adelegate->GetPathForProperty(use_path, nullptr);
1465 		option->adelegate->SetModelData(editor->parameter_widget, use_path, prop_shape);
1466 	}
1467 	else
1468 	{
1469 		// No parameter. Use value.
1470 		if (editor->option_changed) SetOptionValue(property_path, *option, *option_value);
1471 	}
1472 	editor->option_changed = false;
1473 }
1474 
SetOptionValue(const C4PropertyPath & use_path,const C4PropertyDelegateEnum::Option & option,const C4Value & option_value) const1475 void C4PropertyDelegateEnum::SetOptionValue(const C4PropertyPath &use_path, const C4PropertyDelegateEnum::Option &option, const C4Value &option_value) const
1476 {
1477 	// After an enum entry has been selected, set its value
1478 	// Either directly by value or through a function
1479 	// Get serialization base
1480 	const C4PropList *ignore_base_props;
1481 	if (option.force_serialization)
1482 	{
1483 		ignore_base_props = option_value.getPropList();
1484 		if (ignore_base_props) ignore_base_props = (ignore_base_props->IsStatic() ? ignore_base_props->IsStatic()->GetParent() : nullptr);
1485 	}
1486 	else
1487 	{
1488 		ignore_base_props = option.props.getPropList();
1489 	}
1490 	const C4PropListStatic *ignore_base_props_static = ignore_base_props ? ignore_base_props->IsStatic() : nullptr;
1491 	if (option.value_function.GetType() == C4V_Function)
1492 	{
1493 		use_path.SetProperty(FormatString("Call(%s, %s, %s)", option.value_function.GetDataString().getData(), use_path.GetRoot(), option_value.GetDataString(20, ignore_base_props_static).getData()).getData());
1494 	}
1495 	else
1496 	{
1497 		C4PropList *option_props = option.props.getPropList();
1498 		use_path.SetProperty(option_value, ignore_base_props_static);
1499 	}
1500 	factory->GetPropertyModel()->DoOnUpdateCall(use_path, this);
1501 }
1502 
CreateEditor(const C4PropertyDelegateFactory * parent_delegate,QWidget * parent,const QStyleOptionViewItem & option,bool by_selection,bool is_child) const1503 QWidget *C4PropertyDelegateEnum::CreateEditor(const C4PropertyDelegateFactory *parent_delegate, QWidget *parent, const QStyleOptionViewItem &option, bool by_selection, bool is_child) const
1504 {
1505 	Editor *editor = new Editor(parent);
1506 	editor->layout = new QHBoxLayout(editor);
1507 	editor->layout->setContentsMargins(0, 0, 0, 0);
1508 	editor->layout->setMargin(0);
1509 	editor->layout->setSpacing(0);
1510 	editor->updating = true;
1511 	editor->option_box = new C4DeepQComboBox(editor, GetOptionComboBoxButtonType(), allow_editing);
1512 	editor->layout->addWidget(editor->option_box);
1513 	for (auto &option : options) editor->option_box->addItem(option.name->GetCStr());
1514 	editor->option_box->setModel(CreateOptionModel());
1515 	editor->option_box->model()->setParent(editor->option_box);
1516 	// Signal for selecting a new entry from the dropdown menu
1517 	connect(editor->option_box, &C4DeepQComboBox::NewItemSelected, editor, [editor, this](int32_t newval) {
1518 		if (!editor->updating) this->UpdateOptionIndex(editor, newval, nullptr); });
1519 	// Signal for write-in on enum delegates that allow editing
1520 	if (allow_editing)
1521 	{
1522 		connect(editor->option_box, &C4DeepQComboBox::TextChanged, editor, [editor, this](const QString &new_text) {
1523 			if (!editor->updating)
1524 			{
1525 				this->UpdateOptionIndex(editor, GetOptionByValue(C4VString(new_text.toUtf8())), &new_text);
1526 			}
1527 		});
1528 	}
1529 
1530 	editor->updating = false;
1531 	// If created by a selection from a parent enum, show drop down immediately after value has been set
1532 	editor->dropdown_pending = by_selection && is_child;
1533 	return editor;
1534 }
1535 
UpdateOptionIndex(C4PropertyDelegateEnum::Editor * editor,int newval,const QString * custom_text) const1536 void C4PropertyDelegateEnum::UpdateOptionIndex(C4PropertyDelegateEnum::Editor *editor, int newval, const QString *custom_text) const
1537 {
1538 	bool has_changed = false;
1539 	// Change by text entry?
1540 	if (custom_text)
1541 	{
1542 		C4String *last_value_string = editor->last_val.getStr();
1543 		if (!last_value_string || last_value_string->GetData() != custom_text->toUtf8())
1544 		{
1545 			editor->last_val = C4VString(custom_text->toUtf8());
1546 			has_changed = true;
1547 		}
1548 	}
1549 	// Update value and parameter delegate if selection changed
1550 	if (newval != editor->last_selection_index)
1551 	{
1552 		editor->last_selection_index = newval;
1553 		UpdateEditorParameter(editor, !custom_text);
1554 		has_changed = true;
1555 	}
1556 	// Change either by text entry or by dropdown selection: Emit signal to parent
1557 	if (has_changed)
1558 	{
1559 		editor->option_changed = true;
1560 		emit EditorValueChangedSignal(editor);
1561 	}
1562 }
1563 
EnsureOptionDelegateResolved(const Option & option) const1564 void C4PropertyDelegateEnum::EnsureOptionDelegateResolved(const Option &option) const
1565 {
1566 	// Lazy-resolve parameter delegate
1567 	if (!option.adelegate && option.adelegate_val.GetType() != C4V_Nil)
1568 		option.adelegate = factory->GetDelegateByValue(option.adelegate_val);
1569 }
1570 
GetDisplayString(const C4Value & v,class C4Object * obj,bool short_names) const1571 QString C4PropertyDelegateEnum::GetDisplayString(const C4Value &v, class C4Object *obj, bool short_names) const
1572 {
1573 	// Display string from value
1574 	int32_t idx = GetOptionByValue(v);
1575 	if (idx == Editor::INDEX_Custom_Value)
1576 	{
1577 		// Value not found: Default display of strings; full display of nonsense values for debugging purposes.
1578 		C4String *custom_string = v.getStr();
1579 		if (custom_string)
1580 		{
1581 			return QString(custom_string->GetCStr());
1582 		}
1583 		else
1584 		{
1585 			return C4PropertyDelegate::GetDisplayString(v, obj, short_names);
1586 		}
1587 	}
1588 	else
1589 	{
1590 		// Value found: Display option string plus parameter
1591 		const Option &option = options[idx];
1592 		QString result = (short_names ? option.short_name : option.name)->GetCStr();
1593 		// Lazy-resolve parameter delegate
1594 		EnsureOptionDelegateResolved(option);
1595 		if (option.adelegate)
1596 		{
1597 			C4Value param_val = v;
1598 			if (option.value_key.Get())
1599 			{
1600 				param_val.Set0();
1601 				C4PropList *vp = v.getPropList();
1602 				if (vp) vp->GetPropertyByS(option.value_key, &param_val);
1603 			}
1604 			if (!result.isEmpty()) result += " ";
1605 			result += option.adelegate->GetDisplayString(param_val, obj, short_names);
1606 		}
1607 		return result;
1608 	}
1609 }
1610 
GetShapeDelegate(C4Value & val,C4PropertyPath * shape_path) const1611 const C4PropertyDelegateShape *C4PropertyDelegateEnum::GetShapeDelegate(C4Value &val, C4PropertyPath *shape_path) const
1612 {
1613 	// Does this delegate own a shape? Forward decision into selected option.
1614 	int32_t option_idx = GetOptionByValue(val);
1615 	if (option_idx == Editor::INDEX_Custom_Value) return nullptr;
1616 	const Option &option = options[option_idx];
1617 	EnsureOptionDelegateResolved(option);
1618 	if (!option.adelegate) return nullptr;
1619 	if (option.value_key.Get())
1620 	{
1621 		*shape_path = option.adelegate->GetPathForProperty(*shape_path, option.value_key->GetCStr());
1622 		C4PropList *vp = val.getPropList();
1623 		val.Set0();
1624 		if (vp) vp->GetPropertyByS(option.value_key, &val);
1625 	}
1626 	return option.adelegate->GetShapeDelegate(val, shape_path);
1627 }
1628 
Paint(QPainter * painter,const QStyleOptionViewItem & option,const C4Value & val) const1629 bool C4PropertyDelegateEnum::Paint(QPainter *painter, const QStyleOptionViewItem &option, const C4Value &val) const
1630 {
1631 	// Custom painting: Forward to selected child delegate
1632 	int32_t option_idx = GetOptionByValue(val);
1633 	if (option_idx == Editor::INDEX_Custom_Value) return false;
1634 	const Option &selected_option = options[option_idx];
1635 	EnsureOptionDelegateResolved(selected_option);
1636 	if (!selected_option.adelegate) return false;
1637 	if (selected_option.adelegate->HasCustomPaint())
1638 	{
1639 		QStyleOptionViewItem parameter_option = QStyleOptionViewItem(option);
1640 		parameter_option.rect.adjust(parameter_option.rect.width()/2, 0, 0, 0);
1641 		C4Value parameter_val = val;
1642 		if (selected_option.value_key.Get())
1643 		{
1644 			parameter_val.Set0();
1645 			C4PropList *vp = val.getPropList();
1646 			if (vp) vp->GetPropertyByS(selected_option.value_key, &parameter_val);
1647 		}
1648 		selected_option.adelegate->Paint(painter, parameter_option, parameter_val);
1649 	}
1650 	// Always return false to draw self using the standard method
1651 	return false;
1652 }
1653 
IsPasteValid(const C4Value & val) const1654 bool C4PropertyDelegateEnum::IsPasteValid(const C4Value &val) const
1655 {
1656 	// Strings always OK in editable enums
1657 	if (val.GetType() == C4V_String && allow_editing) return true;
1658 	// Must be a valid selection
1659 	int32_t option_idx = GetOptionByValue(val);
1660 	if (option_idx == Editor::INDEX_Custom_Value) return false;
1661 	const Option &option = options[option_idx];
1662 	// Check validity for parameter
1663 	EnsureOptionDelegateResolved(option);
1664 	if (!option.adelegate) return true; // No delegate? Then any value is OK.
1665 	C4Value parameter_val;
1666 	if (option.value_key.Get())
1667 	{
1668 		C4PropList *vp = val.getPropList();
1669 		if (!vp) return false;
1670 		vp->GetPropertyByS(option.value_key, &parameter_val); // if this fails, check parameter against nil
1671 	}
1672 	else
1673 	{
1674 		parameter_val = val;
1675 	}
1676 	return option.adelegate->IsPasteValid(parameter_val);
1677 }
1678 
1679 
1680 /* Definition delegate */
1681 
C4PropertyDelegateDef(const C4PropertyDelegateFactory * factory,C4PropList * props)1682 C4PropertyDelegateDef::C4PropertyDelegateDef(const C4PropertyDelegateFactory *factory, C4PropList *props)
1683 	: C4PropertyDelegateEnum(factory, props)
1684 {
1685 	// nil is always an option
1686 	AddConstOption(empty_name ? empty_name.Get() : ::Strings.RegString("nil"), C4VNull);
1687 	// Collect sorted definitions
1688 	filter_property = props ? props->GetPropertyStr(P_Filter) : nullptr;
1689 	if (filter_property)
1690 	{
1691 		// With filter just create a flat list
1692 		std::vector<C4Def *> defs = ::Definitions.GetAllDefs(filter_property);
1693 		std::sort(defs.begin(), defs.end(), [](C4Def *a, C4Def *b) -> bool {
1694 			return strcmp(a->GetName(), b->GetName()) < 0;
1695 		});
1696 		// Add them
1697 		for (C4Def *def : defs)
1698 		{
1699 			C4RefCntPointer<C4String> option_name = ::Strings.RegString(FormatString("%s (%s)", def->id.ToString(), def->GetName()));
1700 			AddConstOption(option_name, C4Value(def), nullptr);
1701 		}
1702 	}
1703 	else
1704 	{
1705 		// Without filter copy tree from definition list model
1706 		C4ConsoleQtDefinitionListModel *def_list_model = factory->GetDefinitionListModel();
1707 		// Recursively add all defs from model
1708 		AddDefinitions(def_list_model, QModelIndex(), nullptr);
1709 	}
1710 }
1711 
AddDefinitions(C4ConsoleQtDefinitionListModel * def_list_model,QModelIndex parent,C4String * group)1712 void C4PropertyDelegateDef::AddDefinitions(C4ConsoleQtDefinitionListModel *def_list_model, QModelIndex parent, C4String *group)
1713 {
1714 	int32_t count = def_list_model->rowCount(parent);
1715 	for (int32_t i = 0; i < count; ++i)
1716 	{
1717 		QModelIndex index = def_list_model->index(i, 0, parent);
1718 		C4Def *def = def_list_model->GetDefByModelIndex(index);
1719 		C4RefCntPointer<C4String> name = ::Strings.RegString(def_list_model->GetNameByModelIndex(index));
1720 		if (def) AddConstOption(name.Get(), C4Value(def), group);
1721 		if (def_list_model->rowCount(index))
1722 		{
1723 			AddDefinitions(def_list_model, index, group ? ::Strings.RegString(FormatString("%s/%s", group->GetCStr(), name->GetCStr()).getData()) : name.Get());
1724 		}
1725 	}
1726 }
1727 
IsPasteValid(const C4Value & val) const1728 bool C4PropertyDelegateDef::IsPasteValid(const C4Value &val) const
1729 {
1730 	// Must be a definition or nil
1731 	if (val.GetType() == C4V_Nil) return true;
1732 	C4Def *def = val.getDef();
1733 	if (!def) return false;
1734 	// Check filter
1735 	if (filter_property)
1736 	{
1737 		C4Value prop_val;
1738 		if (!def->GetPropertyByS(filter_property, &prop_val)) return false;
1739 		if (!prop_val) return false;
1740 	}
1741 	return true;
1742 }
1743 
1744 
1745 /* Object delegate */
1746 
C4PropertyDelegateObject(const C4PropertyDelegateFactory * factory,C4PropList * props)1747 C4PropertyDelegateObject::C4PropertyDelegateObject(const C4PropertyDelegateFactory *factory, C4PropList *props)
1748 	: C4PropertyDelegateEnum(factory, props), max_nearby_objects(20)
1749 {
1750 	// Settings
1751 	if (props)
1752 	{
1753 		filter = props->GetPropertyStr(P_Filter);
1754 	}
1755 	// Actual object list is created/updated when the editor is created
1756 }
1757 
GetObjectEntryString(C4Object * obj) const1758 C4RefCntPointer<C4String> C4PropertyDelegateObject::GetObjectEntryString(C4Object *obj) const
1759 {
1760 	// Compose object display string from containment(*), name, position (@x,y) and object number (#n)
1761 	return ::Strings.RegString(FormatString("%s%s @%d,%d (#%d)", obj->Contained ? "*" : "", obj->GetName(), (int)obj->GetX(), (int)obj->GetY(), (int)obj->Number));
1762 }
1763 
UpdateObjectList()1764 void C4PropertyDelegateObject::UpdateObjectList()
1765 {
1766 	// Re-create object list from current position
1767 	ClearOptions();
1768 	// Get matching objects first
1769 	std::vector<C4Object *> objects;
1770 	for (C4Object *obj : ::Objects) if (obj->Status)
1771 	{
1772 		C4Value filter_val;
1773 		if (filter)
1774 		{
1775 			if (!obj->GetPropertyByS(filter, &filter_val)) continue;
1776 			if (!filter_val) continue;
1777 		}
1778 		objects.push_back(obj);
1779 	}
1780 	// Get list sorted by distance from selected object
1781 	std::vector<C4Object *> objects_by_distance;
1782 	int32_t cx=0, cy=0;
1783 	if (::Console.EditCursor.GetCurrentSelectionPosition(&cx, &cy))
1784 	{
1785 		objects_by_distance = objects;
1786 		auto ObjDist = [cx, cy](C4Object *o) { return (o->GetX() - cx)*(o->GetX() - cx) + (o->GetY() - cy)*(o->GetY() - cy); };
1787 		std::stable_sort(objects_by_distance.begin(), objects_by_distance.end(), [&ObjDist](C4Object *a, C4Object *b) { return ObjDist(a) < ObjDist(b); });
1788 	}
1789 	size_t num_nearby = objects_by_distance.size();
1790 	bool has_all_objects_list = (num_nearby > max_nearby_objects);
1791 	if (has_all_objects_list) num_nearby = max_nearby_objects;
1792 	// Add actual objects
1793 	ReserveOptions(1 + num_nearby + !!num_nearby + (has_all_objects_list ? objects.size() : 0));
1794 	AddConstOption(::Strings.RegString("nil"), C4VNull); // nil is always an option
1795 	if (num_nearby)
1796 	{
1797 		// TODO: "Select object" entry
1798 		//AddCallbackOption(LoadResStr("IDS_CNS_SELECTOBJECT"));
1799 		// Nearby list
1800 		C4RefCntPointer<C4String> nearby_group;
1801 		// If there are main objects, Create a subgroup. Otherwise, just put all elements into the main group.
1802 		if (has_all_objects_list) nearby_group = ::Strings.RegString(LoadResStr("IDS_CNS_NEARBYOBJECTS"));
1803 		for (int32_t i = 0; i < num_nearby; ++i)
1804 		{
1805 			C4Object *obj = objects_by_distance[i];
1806 			AddConstOption(GetObjectEntryString(obj).Get(), C4VObj(obj), nearby_group.Get());
1807 		}
1808 		// All objects
1809 		if (has_all_objects_list)
1810 		{
1811 			C4RefCntPointer<C4String> all_group = ::Strings.RegString(LoadResStr("IDS_CNS_ALLOBJECTS"));
1812 			for (C4Object *obj : objects) AddConstOption(GetObjectEntryString(obj).Get(), C4VObj(obj), all_group.Get());
1813 		}
1814 	}
1815 }
1816 
CreateEditor(const class C4PropertyDelegateFactory * parent_delegate,QWidget * parent,const QStyleOptionViewItem & option,bool by_selection,bool is_child) const1817 QWidget *C4PropertyDelegateObject::CreateEditor(const class C4PropertyDelegateFactory *parent_delegate, QWidget *parent, const QStyleOptionViewItem &option, bool by_selection, bool is_child) const
1818 {
1819 	// Update object list for created editor
1820 	// (This should be safe since the object delegate cannot contain nested delegates)
1821 	const_cast<C4PropertyDelegateObject *>(this)->UpdateObjectList();
1822 	return C4PropertyDelegateEnum::CreateEditor(parent_delegate, parent, option, by_selection, is_child);
1823 }
1824 
GetDisplayString(const C4Value & v,class C4Object * obj,bool short_names) const1825 QString C4PropertyDelegateObject::GetDisplayString(const C4Value &v, class C4Object *obj, bool short_names) const
1826 {
1827 	C4Object *vobj = v.getObj();
1828 	if (vobj)
1829 	{
1830 		C4RefCntPointer<C4String> s = GetObjectEntryString(vobj);
1831 		return QString(s->GetCStr());
1832 	}
1833 	else
1834 	{
1835 		return QString(v.GetDataString().getData());
1836 	}
1837 }
1838 
IsPasteValid(const C4Value & val) const1839 bool C4PropertyDelegateObject::IsPasteValid(const C4Value &val) const
1840 {
1841 	// Must be an object or nil
1842 	if (val.GetType() == C4V_Nil) return true;
1843 	C4Object *obj = val.getObj();
1844 	if (!obj) return false;
1845 	// Check filter
1846 	if (filter)
1847 	{
1848 		C4Value prop_val;
1849 		if (!obj->GetPropertyByS(filter, &prop_val)) return false;
1850 		if (!prop_val) return false;
1851 	}
1852 	return true;
1853 }
1854 
1855 
1856 /* Sound delegate */
1857 
C4PropertyDelegateSound(const C4PropertyDelegateFactory * factory,C4PropList * props)1858 C4PropertyDelegateSound::C4PropertyDelegateSound(const C4PropertyDelegateFactory *factory, C4PropList *props)
1859 	: C4PropertyDelegateEnum(factory, props)
1860 {
1861 	// Add none-option
1862 	AddConstOption(::Strings.RegString("nil"), C4VNull);
1863 	// Add all sounds as options
1864 	for (C4SoundEffect *fx = ::Application.SoundSystem.GetFirstSound(); fx; fx = fx->Next)
1865 	{
1866 		// Extract group name as path to sound, replacing "::" by "/" for enum groups
1867 		StdStrBuf full_name_s(fx->GetFullName(), true);
1868 		RemoveExtension(&full_name_s);
1869 		const char *full_name = full_name_s.getData();
1870 		const char *base_name = full_name, *pos;
1871 		StdStrBuf group_string;
1872 		while ((pos = SSearch(base_name, "::")))
1873 		{
1874 			if (group_string.getLength()) group_string.AppendChar('/');
1875 			group_string.Append(base_name, pos - base_name - 2);
1876 			base_name = pos;
1877 		}
1878 		C4RefCntPointer<C4String> group;
1879 		if (group_string.getLength()) group = ::Strings.RegString(group_string);
1880 		// Script name: Full name (without extension)
1881 		C4RefCntPointer<C4String> sound_string = ::Strings.RegString(full_name_s);
1882 		// Add the option
1883 		AddConstOption(::Strings.RegString(base_name), C4VString(sound_string.Get()), group.Get(), sound_string.Get());
1884 	}
1885 }
1886 
GetDisplayString(const C4Value & v,class C4Object * obj,bool short_names) const1887 QString C4PropertyDelegateSound::GetDisplayString(const C4Value &v, class C4Object *obj, bool short_names) const
1888 {
1889 	// Always show full sound name
1890 	C4String *val_string = v.getStr();
1891 	return val_string ? QString(val_string->GetCStr()) : QString(v.GetDataString().getData());
1892 }
1893 
IsPasteValid(const C4Value & val) const1894 bool C4PropertyDelegateSound::IsPasteValid(const C4Value &val) const
1895 {
1896 	// Must be nil or a string
1897 	if (val.GetType() == C4V_Nil) return true;
1898 	if (val.GetType() != C4V_String) return false;
1899 	return true;
1900 }
1901 
1902 
1903 /* Boolean delegate */
1904 
C4PropertyDelegateBool(const C4PropertyDelegateFactory * factory,C4PropList * props)1905 C4PropertyDelegateBool::C4PropertyDelegateBool(const C4PropertyDelegateFactory *factory, C4PropList *props)
1906 	: C4PropertyDelegateEnum(factory, props)
1907 {
1908 	// Add boolean options
1909 	ReserveOptions(2);
1910 	AddConstOption(::Strings.RegString(LoadResStr("IDS_CNS_FALSE")), C4VBool(false));
1911 	AddConstOption(::Strings.RegString(LoadResStr("IDS_CNS_TRUE")), C4VBool(true));
1912 }
1913 
GetPropertyValue(const C4Value & container,C4String * key,int32_t index,C4Value * out_val) const1914 bool C4PropertyDelegateBool::GetPropertyValue(const C4Value &container, C4String *key, int32_t index, C4Value *out_val) const
1915 {
1916 	// Force value to bool
1917 	bool success = C4PropertyDelegateEnum::GetPropertyValue(container, key, index, out_val);
1918 	if (out_val->GetType() != C4V_Bool) *out_val = C4VBool(!!*out_val);
1919 	return success;
1920 }
1921 
IsPasteValid(const C4Value & val) const1922 bool C4PropertyDelegateBool::IsPasteValid(const C4Value &val) const
1923 {
1924 	// Must be a boolean
1925 	if (val.GetType() != C4V_Bool) return false;
1926 	return true;
1927 }
1928 
1929 
1930 /* Has-effect delegate */
1931 
C4PropertyDelegateHasEffect(const class C4PropertyDelegateFactory * factory,C4PropList * props)1932 C4PropertyDelegateHasEffect::C4PropertyDelegateHasEffect(const class C4PropertyDelegateFactory *factory, C4PropList *props)
1933 	: C4PropertyDelegateBool(factory, props)
1934 {
1935 	if (props) effect = props->GetPropertyStr(P_Effect);
1936 }
1937 
GetPropertyValue(const C4Value & container,C4String * key,int32_t index,C4Value * out_val) const1938 bool C4PropertyDelegateHasEffect::GetPropertyValue(const C4Value &container, C4String *key, int32_t index, C4Value *out_val) const
1939 {
1940 	const C4Object *obj = container.getObj();
1941 	if (obj && effect)
1942 	{
1943 		bool has_effect = false;
1944 		for (C4Effect *fx = obj->pEffects; fx; fx = fx->pNext)
1945 			if (!fx->IsDead())
1946 				if (!strcmp(fx->GetName(), effect->GetCStr()))
1947 				{
1948 					has_effect = true;
1949 					break;
1950 				}
1951 		*out_val = C4VBool(has_effect);
1952 		return true;
1953 	}
1954 	return false;
1955 }
1956 
1957 
1958 /* C4Value via an enumeration delegate */
1959 
C4PropertyDelegateC4ValueEnum(const C4PropertyDelegateFactory * factory,C4PropList * props)1960 C4PropertyDelegateC4ValueEnum::C4PropertyDelegateC4ValueEnum(const C4PropertyDelegateFactory *factory, C4PropList *props)
1961 	: C4PropertyDelegateEnum(factory, props)
1962 {
1963 	// Add default C4Value selections
1964 	ReserveOptions(10);
1965 	AddTypeOption(::Strings.RegString("nil"), C4V_Nil, C4VNull);
1966 	AddTypeOption(::Strings.RegString("bool"), C4V_Bool, C4VNull, factory->GetDelegateByValue(C4VString("bool")));
1967 	AddTypeOption(::Strings.RegString("int"), C4V_Int, C4VNull, factory->GetDelegateByValue(C4VString("int")));
1968 	AddTypeOption(::Strings.RegString("string"), C4V_String, C4VNull, factory->GetDelegateByValue(C4VString("string")));
1969 	AddTypeOption(::Strings.RegString("array"), C4V_Array, C4VNull, factory->GetDelegateByValue(C4VString("array")));
1970 	AddTypeOption(::Strings.RegString("function"), C4V_Function, C4VNull, factory->GetDelegateByValue(C4VString("function")));
1971 	AddTypeOption(::Strings.RegString("object"), C4V_Object, C4VNull, factory->GetDelegateByValue(C4VString("object")));
1972 	AddTypeOption(::Strings.RegString("def"), C4V_Def, C4VNull, factory->GetDelegateByValue(C4VString("def")));
1973 	AddTypeOption(::Strings.RegString("effect"), C4V_Effect, C4VNull, factory->GetDelegateByValue(C4VString("effect")));
1974 	AddTypeOption(::Strings.RegString("proplist"), C4V_PropList, C4VNull, factory->GetDelegateByValue(C4VString("proplist")));
1975 }
1976 
1977 
1978 /* C4Value via an edit field delegate */
1979 
C4PropertyDelegateC4ValueInputEditor(QWidget * parent)1980 C4PropertyDelegateC4ValueInputEditor::C4PropertyDelegateC4ValueInputEditor(QWidget *parent)
1981 	: QWidget(parent), layout(nullptr), edit(nullptr), extended_button(nullptr), commit_pending(false)
1982 {
1983 	layout = new QHBoxLayout(this);
1984 	layout->setContentsMargins(0, 0, 0, 0);
1985 	layout->setMargin(0);
1986 	layout->setSpacing(0);
1987 	edit = new QLineEdit(this);
1988 	layout->addWidget(edit);
1989 	extended_button = new QPushButton("...", this);
1990 	extended_button->setMaximumWidth(extended_button->fontMetrics().boundingRect("...").width() + 6);
1991 	layout->addWidget(extended_button);
1992 	extended_button->hide();
1993 	edit->setFocus();
1994 	setLayout(layout);
1995 }
1996 
SetEditorData(QWidget * aeditor,const C4Value & val,const C4PropertyPath & property_path) const1997 void C4PropertyDelegateC4ValueInput::SetEditorData(QWidget *aeditor, const C4Value &val, const C4PropertyPath &property_path) const
1998 {
1999 	Editor *editor = static_cast<Editor *>(aeditor);
2000 	editor->edit->setText(val.GetDataString().getData());
2001 	if (val.GetType() == C4V_PropList || val.GetType() == C4V_Array)
2002 	{
2003 		editor->extended_button->show();
2004 		editor->property_path = property_path;
2005 	}
2006 	else
2007 	{
2008 		editor->extended_button->hide();
2009 	}
2010 }
2011 
SetModelData(QObject * aeditor,const C4PropertyPath & property_path,C4ConsoleQtShape * prop_shape) const2012 void C4PropertyDelegateC4ValueInput::SetModelData(QObject *aeditor, const C4PropertyPath &property_path, C4ConsoleQtShape *prop_shape) const
2013 {
2014 	// Only set model data when pressing Enter explicitely; not just when leaving
2015 	Editor *editor = static_cast<Editor *>(aeditor);
2016 	if (editor->commit_pending)
2017 	{
2018 		property_path.SetProperty(editor->edit->text().toUtf8());
2019 		factory->GetPropertyModel()->DoOnUpdateCall(property_path, this);
2020 		editor->commit_pending = false;
2021 	}
2022 }
2023 
CreateEditor(const class C4PropertyDelegateFactory * parent_delegate,QWidget * parent,const QStyleOptionViewItem & option,bool by_selection,bool is_child) const2024 QWidget *C4PropertyDelegateC4ValueInput::CreateEditor(const class C4PropertyDelegateFactory *parent_delegate, QWidget *parent, const QStyleOptionViewItem &option, bool by_selection, bool is_child) const
2025 {
2026 	// Editor is just an edit box plus a "..." button for array/proplist types
2027 	Editor *editor = new Editor(parent);
2028 	// EditingDone only on Return; not just when leaving edit field
2029 	connect(editor->edit, &QLineEdit::returnPressed, editor, [this, editor]() {
2030 		editor->commit_pending = true;
2031 		emit EditingDoneSignal(editor);
2032 	});
2033 	connect(editor->extended_button, &QPushButton::pressed, editor, [this, editor]() {
2034 		C4Value val = editor->property_path.ResolveValue();
2035 		if (val.getPropList() || val.getArray())
2036 		{
2037 			this->factory->GetPropertyModel()->DescendPath(val, val.getPropList(), editor->property_path);
2038 			::Console.EditCursor.InvalidateSelection();
2039 		}
2040 	});
2041 	// Selection in child enum: Direct focus
2042 	if (by_selection && is_child) editor->edit->setFocus();
2043 	return editor;
2044 }
2045 
2046 
2047 /* Areas shown in viewport */
2048 
C4PropertyDelegateShape(const class C4PropertyDelegateFactory * factory,C4PropList * props)2049 C4PropertyDelegateShape::C4PropertyDelegateShape(const class C4PropertyDelegateFactory *factory, C4PropList *props)
2050 	: C4PropertyDelegate(factory, props), clr(0xffff0000)
2051 {
2052 	if (props)
2053 	{
2054 		clr = props->GetPropertyInt(P_Color) | 0xff000000;
2055 	}
2056 }
2057 
SetModelData(QObject * editor,const C4PropertyPath & property_path,C4ConsoleQtShape * prop_shape) const2058 void C4PropertyDelegateShape::SetModelData(QObject *editor, const C4PropertyPath &property_path, C4ConsoleQtShape *prop_shape) const
2059 {
2060 	// Only set shape data if triggered through shape movement signal; ignore update calls from e.g. parent enum editor
2061 	if (!editor)
2062 	{
2063 		if (prop_shape && prop_shape->GetParentDelegate() == this)
2064 		{
2065 			property_path.SetProperty(prop_shape->GetValue());
2066 			factory->GetPropertyModel()->DoOnUpdateCall(property_path, this);
2067 		}
2068 	}
2069 }
2070 
Paint(QPainter * painter,const QStyleOptionViewItem & option,const C4Value & val) const2071 bool C4PropertyDelegateShape::Paint(QPainter *painter, const QStyleOptionViewItem &option, const C4Value &val) const
2072 {
2073 	// Background color
2074 	if (option.state & QStyle::State_Selected)
2075 		painter->fillRect(option.rect, option.palette.highlight());
2076 	else
2077 		painter->fillRect(option.rect, option.palette.base());
2078 	// Draw a frame in shape color
2079 	painter->save();
2080 	QColor frame_color = QColor(QRgb(clr & 0xffffff));
2081 	int32_t width = Clamp<int32_t>(option.rect.height() / 8, 2, 6) &~1;
2082 	QPen rect_pen(QBrush(frame_color), width, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin);
2083 	painter->setPen(rect_pen);
2084 	QRect inner_rect = option.rect.adjusted(width / 2, width / 2, -width / 2, -width / 2);
2085 	if (inner_rect.width() > inner_rect.height())
2086 	{
2087 		// Draw shape in right corner
2088 		inner_rect.adjust(inner_rect.width() - inner_rect.height(), 0, 0, 0);
2089 	}
2090 	// Paint by shape type
2091 	DoPaint(painter, inner_rect);
2092 	// Done painting
2093 	painter->restore();
2094 	return true;
2095 }
2096 
ConnectSignals(C4ConsoleQtShape * shape,const C4PropertyPath & property_path) const2097 void C4PropertyDelegateShape::ConnectSignals(C4ConsoleQtShape *shape, const C4PropertyPath &property_path) const
2098 {
2099 	connect(shape, &C4ConsoleQtShape::ShapeDragged, this, [this, shape, property_path]() {
2100 		this->SetModelData(nullptr, property_path, shape);
2101 	});
2102 }
2103 
2104 /* Areas shown in viewport: Rectangle */
2105 
C4PropertyDelegateRect(const class C4PropertyDelegateFactory * factory,C4PropList * props)2106 C4PropertyDelegateRect::C4PropertyDelegateRect(const class C4PropertyDelegateFactory *factory, C4PropList *props)
2107 	: C4PropertyDelegateShape(factory, props)
2108 {
2109 	if (props)
2110 	{
2111 		storage = props->GetPropertyStr(P_Storage);
2112 	}
2113 }
2114 
DoPaint(QPainter * painter,const QRect & inner_rect) const2115 void C4PropertyDelegateRect::DoPaint(QPainter *painter, const QRect &inner_rect) const
2116 {
2117 	painter->drawRect(inner_rect);
2118 }
2119 
IsPasteValid(const C4Value & val) const2120 bool C4PropertyDelegateRect::IsPasteValid(const C4Value &val) const
2121 {
2122 	// Check storage as prop list
2123 	if (storage)
2124 	{
2125 		// Proplist-stored rect must have defined properties
2126 		C4PropertyName def_property_names[2][4] = { { P_x, P_y, P_wdt, P_hgt },{ P_X, P_Y, P_Wdt, P_Hgt } };
2127 		C4PropertyName *property_names = nullptr;
2128 		if (storage == &::Strings.P[P_proplist])
2129 		{
2130 			property_names = def_property_names[0];
2131 		}
2132 		else if (storage == &::Strings.P[P_Proplist])
2133 		{
2134 			property_names = def_property_names[1];
2135 		}
2136 		if (property_names)
2137 		{
2138 			C4PropList *val_proplist = val.getPropList();
2139 			if (!val_proplist) return false;
2140 			for (int32_t i = 0; i < 4; ++i)
2141 			{
2142 				C4Value propval;
2143 				if (!val_proplist->GetProperty(property_names[i], &propval)) return false;
2144 				if (propval.GetType() != C4V_Int) return false;
2145 			}
2146 			// extra properties are OK
2147 		}
2148 		return true;
2149 	}
2150 	// Check storage as array: Expect array with four elements. Width and height non-negative.
2151 	C4ValueArray *val_arr = val.getArray();
2152 	if (!val_arr || val_arr->GetSize() != 4) return false;
2153 	for (int32_t i = 0; i < 4; ++i) if (val_arr->GetItem(i).GetType() != C4V_Int) return false;
2154 	if (val_arr->GetItem(2)._getInt() < 0) return false;
2155 	if (val_arr->GetItem(3)._getInt() < 0) return false;
2156 	return true;
2157 }
2158 
2159 
2160 /* Areas shown in viewport: Circle */
2161 
C4PropertyDelegateCircle(const class C4PropertyDelegateFactory * factory,C4PropList * props)2162 C4PropertyDelegateCircle::C4PropertyDelegateCircle(const class C4PropertyDelegateFactory *factory, C4PropList *props)
2163 	: C4PropertyDelegateShape(factory, props)
2164 {
2165 	if (props)
2166 	{
2167 		can_move_center = props->GetPropertyBool(P_CanMoveCenter);
2168 	}
2169 }
2170 
DoPaint(QPainter * painter,const QRect & inner_rect) const2171 void C4PropertyDelegateCircle::DoPaint(QPainter *painter, const QRect &inner_rect) const
2172 {
2173 	painter->drawEllipse(inner_rect);
2174 	if (can_move_center) painter->drawPoint(inner_rect.center());
2175 }
2176 
IsPasteValid(const C4Value & val) const2177 bool C4PropertyDelegateCircle::IsPasteValid(const C4Value &val) const
2178 {
2179 	// Circle radius stored as single non-negative int
2180 	if (!can_move_center) return (val.GetType() == C4V_Int) && (val.getInt() >= 0);
2181 	// Circle+Center stored as array with three elements (radius, x, y)
2182 	C4ValueArray *val_arr = val.getArray();
2183 	if (!val_arr || val_arr->GetSize() != 3) return false;
2184 	for (int32_t i = 0; i < 3; ++i) if (val_arr->GetItem(i).GetType() != C4V_Int) return false;
2185 	if (val_arr->GetItem(0)._getInt() < 0) return false;
2186 	return true;
2187 }
2188 
2189 
2190 /* Areas shown in viewport: Point */
2191 
C4PropertyDelegatePoint(const class C4PropertyDelegateFactory * factory,C4PropList * props)2192 C4PropertyDelegatePoint::C4PropertyDelegatePoint(const class C4PropertyDelegateFactory *factory, C4PropList *props)
2193 	: C4PropertyDelegateShape(factory, props)
2194 {
2195 	if (props)
2196 	{
2197 		horizontal_fix = props->GetPropertyBool(P_HorizontalFix);
2198 		vertical_fix = props->GetPropertyBool(P_VerticalFix);
2199 	}
2200 }
2201 
DoPaint(QPainter * painter,const QRect & inner_rect) const2202 void C4PropertyDelegatePoint::DoPaint(QPainter *painter, const QRect &inner_rect) const
2203 {
2204 	QPoint ctr = inner_rect.center();
2205 	int r = inner_rect.height() * 7 / 20;
2206 	if (horizontal_fix && !vertical_fix)
2207 	{
2208 		painter->drawLine(ctr + QPoint(0, -r), ctr + QPoint(0, +r));
2209 		painter->drawLine(ctr + QPoint(-r / 2, -r), ctr + QPoint(+r / 2, -r));
2210 		painter->drawLine(ctr + QPoint(-r / 2, +r), ctr + QPoint(+r / 2, +r));
2211 	}
2212 	else if (vertical_fix && !horizontal_fix)
2213 	{
2214 		painter->drawLine(ctr + QPoint(-r, 0), ctr + QPoint(+r, 0));
2215 		painter->drawLine(ctr + QPoint(-r, -r / 2), ctr + QPoint(-r, +r / 2));
2216 		painter->drawLine(ctr + QPoint(+r, -r / 2), ctr + QPoint(+r, +r / 2));
2217 	}
2218 	else
2219 	{
2220 		if (!horizontal_fix)
2221 		{
2222 			painter->drawLine(ctr + QPoint(-r, -r), ctr + QPoint(+r, +r));
2223 		}
2224 		painter->drawLine(ctr + QPoint(+r, -r), ctr + QPoint(-r, +r));
2225 		painter->drawEllipse(inner_rect);
2226 	}
2227 }
2228 
IsPasteValid(const C4Value & val) const2229 bool C4PropertyDelegatePoint::IsPasteValid(const C4Value &val) const
2230 {
2231 	// Point stored as array with two elements
2232 	C4ValueArray *val_arr = val.getArray();
2233 	if (!val_arr || val_arr->GetSize() != 2) return false;
2234 	for (int32_t i = 0; i < 2; ++i) if (val_arr->GetItem(i).GetType() != C4V_Int) return false;
2235 	return true;
2236 }
2237 
2238 
2239 /* Areas shown in viewport: Graph */
2240 
C4PropertyDelegateGraph(const class C4PropertyDelegateFactory * factory,C4PropList * props)2241 C4PropertyDelegateGraph::C4PropertyDelegateGraph(const class C4PropertyDelegateFactory *factory, C4PropList *props)
2242 	: C4PropertyDelegateShape(factory, props)
2243 {
2244 	if (props)
2245 	{
2246 		horizontal_fix = props->GetPropertyBool(P_HorizontalFix);
2247 		vertical_fix = props->GetPropertyBool(P_VerticalFix);
2248 		structure_fix = props->GetPropertyBool(P_StructureFix);
2249 	}
2250 }
2251 
DoPaint(QPainter * painter,const QRect & inner_rect) const2252 void C4PropertyDelegateGraph::DoPaint(QPainter *painter, const QRect &inner_rect) const
2253 {
2254 	// Draw symbol as a bunch of connected lines
2255 	QPoint ctr = inner_rect.center();
2256 	int r = inner_rect.height() * 7 / 20;
2257 	painter->drawLine(ctr, ctr + QPoint(-r / 2, -r));
2258 	painter->drawLine(ctr, ctr + QPoint(+r / 2, -r));
2259 	painter->drawLine(ctr, ctr + QPoint(0, +r));
2260 }
2261 
IsVertexPasteValid(const C4Value & val) const2262 bool C4PropertyDelegateGraph::IsVertexPasteValid(const C4Value &val) const
2263 {
2264 	// Check that it's an array of at least one point
2265 	const C4ValueArray *arr = val.getArray();
2266 	if (!arr || !arr->GetSize()) return false;
2267 	// Check validity of each point
2268 	const int32_t n_props = 2;
2269 	C4PropertyName property_names[n_props] = { P_X, P_Y };
2270 	for (int32_t i_pt = 0; i_pt < arr->GetSize(); ++i_pt)
2271 	{
2272 		const C4Value &pt = arr->GetItem(i_pt);
2273 		const C4PropList *ptp = pt.getPropList();
2274 		if (!ptp) return false;
2275 		for (auto & property_name : property_names)
2276 		{
2277 			C4Value ptprop;
2278 			if (!ptp->GetProperty(property_name, &ptprop)) return false;
2279 			if (ptprop.GetType() != C4V_Int) return false;
2280 		}
2281 	}
2282 	return true;
2283 }
2284 
IsEdgePasteValid(const C4Value & val) const2285 bool C4PropertyDelegateGraph::IsEdgePasteValid(const C4Value &val) const
2286 {
2287 	// Check that it's an array
2288 	// Empty is OK; it could be a graph with one vertex and no edges
2289 	const C4ValueArray *arr = val.getArray();
2290 	if (!arr || !arr->GetSize()) return false;
2291 	// Check validity of each edge
2292 	for (int32_t i_pt = 0; i_pt < arr->GetSize(); ++i_pt)
2293 	{
2294 		const C4Value pt = arr->GetItem(i_pt);
2295 		const C4ValueArray *pta;
2296 		const C4PropList *ptp = pt.getPropList();
2297 		if (!ptp) return false;
2298 		pta = ptp->GetPropertyArray(P_Vertices);
2299 		if (!pta) return false;
2300 		// Needs two vertices (may have more values which are ignored)
2301 		if (pta->GetSize() < 2) return false;
2302 	}
2303 	return true;
2304 }
2305 
IsPasteValid(const C4Value & val) const2306 bool C4PropertyDelegateGraph::IsPasteValid(const C4Value &val) const
2307 {
2308 	// Unfortunately, there is no good way to determine the correct value for fixed structure / position graph pastes
2309 	// So just reject pastes for now
2310 	// (TODO: Could store a default structure in a property and compare to that)
2311 	if (horizontal_fix || vertical_fix || structure_fix) return false;
2312 	// Check storage as prop list
2313 	const int32_t n_props = 2;
2314 	C4Value prop_vals[n_props]; // vertices & edges
2315 	C4PropertyName property_names[n_props] = { P_Vertices, P_Edges };
2316 	C4PropList *val_proplist = val.getPropList();
2317 	if (!val_proplist) return false;
2318 	for (int32_t i = 0; i < n_props; ++i)
2319 	{
2320 		val_proplist->GetProperty(property_names[i], &prop_vals[i]);
2321 	}
2322 	// extra properties are OK
2323 	// Check validity of vertices and edges
2324 	return IsVertexPasteValid(prop_vals[0]) && IsEdgePasteValid(prop_vals[1]);
2325 }
2326 
ConnectSignals(C4ConsoleQtShape * shape,const C4PropertyPath & property_path) const2327 void C4PropertyDelegateGraph::ConnectSignals(C4ConsoleQtShape *shape, const C4PropertyPath &property_path) const
2328 {
2329 	C4ConsoleQtGraph *shape_graph = static_cast<C4ConsoleQtGraph *>(shape);
2330 	connect(shape_graph, &C4ConsoleQtGraph::GraphEdit, this, [this, shape, property_path](C4ControlEditGraph::Action action, int32_t index, int32_t x, int32_t y) {
2331 		// Send graph editing via queue
2332 		::Control.DoInput(CID_EditGraph, new C4ControlEditGraph(property_path.GetGetPath(), action, index, x, y), CDT_Decide);
2333 		// Also send update callback to root object
2334 		factory->GetPropertyModel()->DoOnUpdateCall(property_path, this);
2335 	});
2336 	connect(shape, &C4ConsoleQtShape::BorderSelectionChanged, this, []() {
2337 		// Different part of the shape selected: Refresh info on next update
2338 		::Console.EditCursor.InvalidateSelection();
2339 	});
2340 }
2341 
2342 
2343 
2344 /* Areas shown in viewport: Polyline */
2345 
C4PropertyDelegatePolyline(const class C4PropertyDelegateFactory * factory,C4PropList * props)2346 C4PropertyDelegatePolyline::C4PropertyDelegatePolyline(const class C4PropertyDelegateFactory *factory, C4PropList *props)
2347 	: C4PropertyDelegateGraph(factory, props)
2348 {
2349 }
2350 
DoPaint(QPainter * painter,const QRect & inner_rect) const2351 void C4PropertyDelegatePolyline::DoPaint(QPainter *painter, const QRect &inner_rect) const
2352 {
2353 	// Draw symbol as a sequence of connected lines
2354 	QPoint ctr = inner_rect.center();
2355 	int r = inner_rect.height() * 7 / 20;
2356 	painter->drawLine(ctr + QPoint(-r, +r), ctr + QPoint(-r/3, -r));
2357 	painter->drawLine(ctr + QPoint(-r / 3, -r), ctr + QPoint(+r / 3, +r));
2358 	painter->drawLine(ctr + QPoint(+r / 3, +r), ctr + QPoint(+r, -r));
2359 }
2360 
IsPasteValid(const C4Value & val) const2361 bool C4PropertyDelegatePolyline::IsPasteValid(const C4Value &val) const
2362 {
2363 	// Expect just a vertex array
2364 	return IsVertexPasteValid(val);
2365 }
2366 
2367 
2368 /* Areas shown in viewport: Closed polyon */
2369 
C4PropertyDelegatePolygon(const class C4PropertyDelegateFactory * factory,C4PropList * props)2370 C4PropertyDelegatePolygon::C4PropertyDelegatePolygon(const class C4PropertyDelegateFactory *factory, C4PropList *props)
2371 	: C4PropertyDelegateGraph(factory, props)
2372 {
2373 }
2374 
DoPaint(QPainter * painter,const QRect & inner_rect) const2375 void C4PropertyDelegatePolygon::DoPaint(QPainter *painter, const QRect &inner_rect) const
2376 {
2377 	// Draw symbol as a parallelogram
2378 	QPoint ctr = inner_rect.center();
2379 	int r = inner_rect.height() * 7 / 20;
2380 	painter->drawLine(ctr + QPoint(-r * 3 / 2, +r), ctr + QPoint(-r, -r));
2381 	painter->drawLine(ctr + QPoint(-r, -r), ctr + QPoint(+r * 3 / 2, -r));
2382 	painter->drawLine(ctr + QPoint(+r * 3 / 2, -r), ctr + QPoint(+r, +r));
2383 	painter->drawLine(ctr + QPoint(+r, +r), ctr + QPoint(-r * 3 / 2, +r));
2384 }
2385 
IsPasteValid(const C4Value & val) const2386 bool C4PropertyDelegatePolygon::IsPasteValid(const C4Value &val) const
2387 {
2388 	// Expect just a vertex array
2389 	return IsVertexPasteValid(val);
2390 }
2391 
2392 
2393 /* Delegate factory: Create delegates based on the C4Value type */
2394 
C4PropertyDelegateFactory()2395 C4PropertyDelegateFactory::C4PropertyDelegateFactory() : effect_delegate(this, nullptr)
2396 {
2397 
2398 }
2399 
CreateDelegateByPropList(C4PropList * props) const2400 C4PropertyDelegate *C4PropertyDelegateFactory::CreateDelegateByPropList(C4PropList *props) const
2401 {
2402 	if (props)
2403 	{
2404 		const C4String *str = props->GetPropertyStr(P_Type);
2405 		if (str)
2406 		{
2407 			// create default base types
2408 			if (str->GetData() == "int") return new C4PropertyDelegateInt(this, props);
2409 			if (str->GetData() == "string") return new C4PropertyDelegateString(this, props);
2410 			if (str->GetData() == "array") return new C4PropertyDelegateArray(this, props);
2411 			if (str->GetData() == "proplist") return new C4PropertyDelegatePropList(this, props);
2412 			if (str->GetData() == "color") return new C4PropertyDelegateColor(this, props);
2413 			if (str->GetData() == "def") return new C4PropertyDelegateDef(this, props);
2414 			if (str->GetData() == "object") return new C4PropertyDelegateObject(this, props);
2415 			if (str->GetData() == "enum") return new C4PropertyDelegateEnum(this, props);
2416 			if (str->GetData() == "sound") return new C4PropertyDelegateSound(this, props);
2417 			if (str->GetData() == "bool") return new C4PropertyDelegateBool(this, props);
2418 			if (str->GetData() == "has_effect") return new C4PropertyDelegateHasEffect(this, props);
2419 			if (str->GetData() == "c4valueenum") return new C4PropertyDelegateC4ValueEnum(this, props);
2420 			if (str->GetData() == "rect") return new C4PropertyDelegateRect(this, props);
2421 			if (str->GetData() == "circle") return new C4PropertyDelegateCircle(this, props);
2422 			if (str->GetData() == "point") return new C4PropertyDelegatePoint(this, props);
2423 			if (str->GetData() == "graph") return new C4PropertyDelegateGraph(this, props);
2424 			if (str->GetData() == "polyline") return new C4PropertyDelegatePolyline(this, props);
2425 			if (str->GetData() == "polygon") return new C4PropertyDelegatePolygon(this, props);
2426 			if (str->GetData() == "any") return new C4PropertyDelegateC4ValueInput(this, props);
2427 			// unknown type
2428 			LogF("Invalid delegate type: %s.", str->GetCStr());
2429 		}
2430 	}
2431 	// Default fallback
2432 	return new C4PropertyDelegateC4ValueInput(this, props);
2433 }
2434 
GetDelegateByValue(const C4Value & val) const2435 C4PropertyDelegate *C4PropertyDelegateFactory::GetDelegateByValue(const C4Value &val) const
2436 {
2437 	auto iter = delegates.find(val.getPropList());
2438 	if (iter != delegates.end()) return iter->second.get();
2439 	C4PropertyDelegate *new_delegate = CreateDelegateByPropList(val.getPropList());
2440 	delegates.insert(std::make_pair(val.getPropList(), std::unique_ptr<C4PropertyDelegate>(new_delegate)));
2441 	return new_delegate;
2442 }
2443 
GetDelegateByIndex(const QModelIndex & index) const2444 C4PropertyDelegate *C4PropertyDelegateFactory::GetDelegateByIndex(const QModelIndex &index) const
2445 {
2446 	C4ConsoleQtPropListModel::Property *prop = property_model->GetPropByIndex(index);
2447 	if (!prop) return nullptr;
2448 	if (!prop->delegate) prop->delegate = GetDelegateByValue(prop->delegate_info);
2449 	return prop->delegate;
2450 }
2451 
ClearDelegates()2452 void C4PropertyDelegateFactory::ClearDelegates()
2453 {
2454 	delegates.clear();
2455 }
2456 
EditorValueChanged(QWidget * editor)2457 void C4PropertyDelegateFactory::EditorValueChanged(QWidget *editor)
2458 {
2459 	emit commitData(editor);
2460 }
2461 
EditingDone(QWidget * editor)2462 void C4PropertyDelegateFactory::EditingDone(QWidget *editor)
2463 {
2464 	emit commitData(editor);
2465 	//emit closeEditor(editor); - done by qt somewhere else...
2466 }
2467 
setEditorData(QWidget * editor,const QModelIndex & index) const2468 void C4PropertyDelegateFactory::setEditorData(QWidget *editor, const QModelIndex &index) const
2469 {
2470 	// Put property value from proplist into editor
2471 	C4PropertyDelegate *d = GetDelegateByIndex(index);
2472 	if (!CheckCurrentEditor(d, editor)) return;
2473 	// Fetch property only first time - ignore further updates to the same value to simplify editing
2474 	C4ConsoleQtPropListModel::Property *prop = property_model->GetPropByIndex(index);
2475 	if (!prop) return;
2476 	C4Value val;
2477 	d->GetPropertyValue(prop->parent_value, prop->key, index.row(), &val);
2478 	if (!prop->about_to_edit && val == last_edited_value) return;
2479 	last_edited_value = val;
2480 	prop->about_to_edit = false;
2481 	d->SetEditorData(editor, val, d->GetPathForProperty(prop));
2482 }
2483 
setModelData(QWidget * editor,QAbstractItemModel * model,const QModelIndex & index) const2484 void C4PropertyDelegateFactory::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
2485 {
2486 	// Fetch property value from editor and set it into proplist
2487 	C4PropertyDelegate *d = GetDelegateByIndex(index);
2488 	if (!CheckCurrentEditor(d, editor)) return;
2489 	C4ConsoleQtPropListModel::Property *prop = property_model->GetPropByIndex(index);
2490 	SetPropertyData(d, editor, prop);
2491 }
2492 
SetPropertyData(const C4PropertyDelegate * d,QObject * editor,C4ConsoleQtPropListModel::Property * editor_prop) const2493 void C4PropertyDelegateFactory::SetPropertyData(const C4PropertyDelegate *d, QObject *editor, C4ConsoleQtPropListModel::Property *editor_prop) const
2494 {
2495 	// Set according to delegate
2496 	const C4PropertyPath set_path = d->GetPathForProperty(editor_prop);
2497 	d->SetModelData(editor, set_path, editor_prop->shape ? editor_prop->shape->Get() : nullptr);
2498 }
2499 
createEditor(QWidget * parent,const QStyleOptionViewItem & option,const QModelIndex & index) const2500 QWidget *C4PropertyDelegateFactory::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
2501 {
2502 	C4PropertyDelegate *d = GetDelegateByIndex(index);
2503 	if (!d) return nullptr;
2504 	C4ConsoleQtPropListModel::Property *prop = property_model->GetPropByIndex(index);
2505 	prop->about_to_edit = true;
2506 	QWidget *editor = d->CreateEditor(this, parent, option, true, false);
2507 	// Connect value change signals (if editing is possible for this property)
2508 	// For some reason, commitData needs a non-const pointer
2509 	if (editor)
2510 	{
2511 		connect(d, &C4PropertyDelegate::EditorValueChangedSignal, editor, [editor, this](QWidget *signal_editor) {
2512 			if (signal_editor == editor) const_cast<C4PropertyDelegateFactory *>(this)->EditorValueChanged(editor);
2513 		});
2514 		connect(d, &C4PropertyDelegate::EditingDoneSignal, editor, [editor, this](QWidget *signal_editor) {
2515 			if (signal_editor == editor) const_cast<C4PropertyDelegateFactory *>(this)->EditingDone(editor);
2516 		});
2517 	}
2518 	current_editor = editor;
2519 	current_editor_delegate = d;
2520 	return editor;
2521 }
2522 
destroyEditor(QWidget * editor,const QModelIndex & index) const2523 void C4PropertyDelegateFactory::destroyEditor(QWidget *editor, const QModelIndex &index) const
2524 {
2525 	if (editor == current_editor)
2526 	{
2527 		current_editor = nullptr;
2528 		current_editor_delegate = nullptr;
2529 		::Console.EditCursor.SetHighlightedObject(nullptr);
2530 	}
2531 	QStyledItemDelegate::destroyEditor(editor, index);
2532 }
2533 
updateEditorGeometry(QWidget * editor,const QStyleOptionViewItem & option,const QModelIndex & index) const2534 void C4PropertyDelegateFactory::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
2535 {
2536 	C4PropertyDelegate *d = GetDelegateByIndex(index);
2537 	if (!CheckCurrentEditor(d, editor)) return;
2538 	return d->UpdateEditorGeometry(editor, option);
2539 }
2540 
sizeHint(const QStyleOptionViewItem & option,const QModelIndex & index) const2541 QSize C4PropertyDelegateFactory::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
2542 {
2543 	int height = QApplication::fontMetrics().height() + 4;
2544 	return QSize(100, height);
2545 }
2546 
paint(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const2547 void C4PropertyDelegateFactory::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
2548 {
2549 	// Delegate has custom painting?
2550 	C4ConsoleQtPropListModel::Property *prop = property_model->GetPropByIndex(index);
2551 	C4PropertyDelegate *d = GetDelegateByIndex(index);
2552 	if (d && prop && d->HasCustomPaint())
2553 	{
2554 		C4Value val;
2555 		d->GetPropertyValue(prop->parent_value, prop->key, index.row(), &val);
2556 		if (d->Paint(painter, option, val)) return;
2557 	}
2558 	// Otherwise use default paint implementation
2559 	QStyledItemDelegate::paint(painter, option, index);
2560 }
2561 
OnPropListChanged()2562 void C4PropertyDelegateFactory::OnPropListChanged()
2563 {
2564 	if (current_editor) emit closeEditor(current_editor);
2565 }
2566 
CheckCurrentEditor(C4PropertyDelegate * d,QWidget * editor) const2567 bool C4PropertyDelegateFactory::CheckCurrentEditor(C4PropertyDelegate *d, QWidget *editor) const
2568 {
2569 	if (!d || (editor && editor != current_editor) || d != current_editor_delegate)
2570 	{
2571 		//const_cast<C4PropertyDelegateFactory *>(this)->emit closeEditor(current_editor);
2572 		destroyEditor(current_editor, QModelIndex());
2573 		return false;
2574 	}
2575 	return true;
2576 }
2577 
2578 static const QString property_mime_type("application/OpenClonkProperty");
2579 
CopyToClipboard(const QModelIndex & index)2580 void C4PropertyDelegateFactory::CopyToClipboard(const QModelIndex &index)
2581 {
2582 	// Re-resolve property. May have shifted while the menu was open
2583 	C4ConsoleQtPropListModel::Property *prop = property_model->GetPropByIndex(index);
2584 	C4PropertyDelegate *d = GetDelegateByIndex(index);
2585 	if (!prop || !d) return;
2586 	// Get data to copy
2587 	C4Value val;
2588 	d->GetPropertyValue(prop->parent_value, prop->key, index.row(), &val);
2589 	StdStrBuf data_str(val.GetDataString(99999));
2590 	// Copy it as an internal mime type and text
2591 	// Presence of the internal type shows that this is a copied property so it can be safely evaluate without sync problems
2592 	QClipboard *clipboard = QApplication::clipboard();
2593 	clipboard->clear();
2594 	std::unique_ptr<QMimeData> data(new QMimeData());
2595 	data->setData(property_mime_type, QByteArray(data_str.getData(), data_str.getSize()));
2596 	data->setText(data_str.getData());
2597 	clipboard->setMimeData(data.release());
2598 }
2599 
PasteFromClipboard(const QModelIndex & index,bool check_only)2600 bool C4PropertyDelegateFactory::PasteFromClipboard(const QModelIndex &index, bool check_only)
2601 {
2602 	// Re-resolve property. May have shifted while the menu was open
2603 	C4ConsoleQtPropListModel::Property *prop = property_model->GetPropByIndex(index);
2604 	C4PropertyDelegate *d = GetDelegateByIndex(index);
2605 	if (!prop || !d) return false;
2606 	// Check value to paste
2607 	QClipboard *clipboard = QApplication::clipboard();
2608 	const QMimeData *data = clipboard->mimeData();
2609 	if (!data) return false; // empty clipboard
2610 	// Prefer copied property; fall back to text
2611 	StdStrBuf str_data;
2612 	if (data->hasFormat(property_mime_type))
2613 	{
2614 		QByteArray prop_data = data->data(property_mime_type);
2615 		str_data.Copy(prop_data);
2616 		// Check data type
2617 		C4Value val = ::AulExec.DirectExec(&::ScriptEngine, str_data.getData(), "paste check", false, nullptr, false);
2618 		if (!d->IsPasteValid(val)) return false;
2619 	}
2620 	else if (data->hasText())
2621 	{
2622 		// Text can always be pasted.
2623 		// Cannot perform a type check here because a function may have been copied that affects sync.
2624 		QString text = data->text();
2625 		str_data.Copy(text.toUtf8());
2626 	}
2627 	else
2628 	{
2629 		// Unknown data type in clipboard. Cannot paste.
2630 		return false;
2631 	}
2632 	if (check_only) return true;
2633 	// Alright, paste!
2634 	d->GetPathForProperty(prop).SetProperty(str_data.getData());
2635 	return true;
2636 }
2637 
editorEvent(QEvent * event,QAbstractItemModel * model,const QStyleOptionViewItem & option,const QModelIndex & index)2638 bool C4PropertyDelegateFactory::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index)
2639 {
2640 	// Custom context menu on item
2641 	// I would like to use the regular context menu functions of Qt on the parent widget
2642 	// but something is eating the right-click event before it triggers a context event.
2643 	// So just hack it on right click.
2644 	// Button check
2645 	if (event->type() == QEvent::Type::MouseButtonPress)
2646 	{
2647 		QMouseEvent *mev = static_cast<QMouseEvent *>(event);
2648 		if (mev->button() == Qt::MouseButton::RightButton)
2649 		{
2650 			// Item check
2651 			C4ConsoleQtPropListModel::Property *prop = property_model->GetPropByIndex(index);
2652 			C4PropertyDelegate *d = GetDelegateByIndex(index);
2653 			if (d && prop)
2654 			{
2655 				// Context menu on a valid property: Show copy+paste menu
2656 				QMenu *context = new QMenu(const_cast<QWidget *>(option.widget));
2657 				QAction *copy_action = new QAction(LoadResStr("IDS_DLG_COPY"), context);
2658 				QAction *paste_action = new QAction(LoadResStr("IDS_DLG_PASTE"), context);
2659 				QModelIndex index_copy(index);
2660 				connect(copy_action, &QAction::triggered, this, [this, index_copy]() {
2661 					this->CopyToClipboard(index_copy);
2662 				});
2663 				connect(paste_action, &QAction::triggered, this, [this, index_copy]() {
2664 					this->PasteFromClipboard(index_copy, false);
2665 				});
2666 				paste_action->setEnabled(PasteFromClipboard(index_copy, true)); // Paste grayed out if not valid
2667 				context->addAction(copy_action);
2668 				context->addAction(paste_action);
2669 				context->popup(mev->globalPos());
2670 				context->connect(context, &QMenu::aboutToHide, context, &QWidget::deleteLater);
2671 				// It's easier to see which item is affected when it's selected
2672 				QItemSelectionModel *sel_model = property_model->GetSelectionModel();
2673 				QItemSelection new_sel;
2674 				new_sel.select(model->index(index.row(), 0, index.parent()), index);
2675 				sel_model->select(new_sel, QItemSelectionModel::SelectionFlag::SelectCurrent);
2676 				return true;
2677 			}
2678 		}
2679 	}
2680 	return QStyledItemDelegate::editorEvent(event, model, option, index);
2681 }
2682 
2683 
2684 /* Proplist table view */
2685 
C4ConsoleQtPropListModel(C4PropertyDelegateFactory * delegate_factory)2686 C4ConsoleQtPropListModel::C4ConsoleQtPropListModel(C4PropertyDelegateFactory *delegate_factory)
2687 	: delegate_factory(delegate_factory), selection_model(nullptr)
2688 {
2689 	header_font.setBold(true);
2690 	important_property_font.setBold(true);
2691 	connect(this, &C4ConsoleQtPropListModel::ProplistChanged, this, &C4ConsoleQtPropListModel::UpdateSelection, Qt::QueuedConnection);
2692 	layout_valid = false;
2693 }
2694 
2695 C4ConsoleQtPropListModel::~C4ConsoleQtPropListModel() = default;
2696 
AddPropertyGroup(C4PropList * add_proplist,int32_t group_index,QString name,C4PropList * target_proplist,const C4PropertyPath & group_target_path,C4Object * base_object,C4String * default_selection,int32_t * default_selection_index)2697 bool C4ConsoleQtPropListModel::AddPropertyGroup(C4PropList *add_proplist, int32_t group_index, QString name, C4PropList *target_proplist, const C4PropertyPath &group_target_path, C4Object *base_object, C4String *default_selection, int32_t *default_selection_index)
2698 {
2699 	// Add all properties from this EditorProps group
2700 	std::vector<C4String *> property_names = add_proplist->GetUnsortedProperties(nullptr);
2701 	if (!property_names.size()) return false;
2702 	// Prepare group array
2703 	if (property_groups.size() == group_index)
2704 	{
2705 		layout_valid = false;
2706 		property_groups.resize(group_index + 1);
2707 	}
2708 	PropertyGroup &properties = property_groups[group_index];
2709 	C4PropListStatic *proplist_static = add_proplist->IsStatic();
2710 	// Resolve properties
2711 	struct PropAndKey
2712 	{
2713 		C4PropList *prop;
2714 		C4String *key;
2715 		int32_t priority;
2716 		C4String *name;
2717 
2718 		PropAndKey(C4PropList *prop, C4String *key, int32_t priority, C4String *name)
2719 			: prop(prop), key(key), priority(priority), name(name) {}
2720 	};
2721 	std::vector<PropAndKey> new_properties_resolved;
2722 	new_properties_resolved.reserve(property_names.size());
2723 	for (C4String *prop_name : property_names)
2724 	{
2725 		C4Value prop_val;
2726 		add_proplist->GetPropertyByS(prop_name, &prop_val);
2727 		C4PropList *prop = prop_val.getPropList();
2728 		if (prop)
2729 		{
2730 			C4String *name = prop->GetPropertyStr(P_Name);
2731 			if (!name) name = prop_name;
2732 			int32_t priority = prop->GetPropertyInt(P_Priority);
2733 			new_properties_resolved.emplace_back(PropAndKey({ prop, prop_name, priority, name }));
2734 		}
2735 	}
2736 	// Sort by priority primarily and name secondarily
2737 	std::sort(new_properties_resolved.begin(), new_properties_resolved.end(), [](const PropAndKey &a, const PropAndKey &b) -> bool {
2738 		if (a.priority != b.priority) return a.priority > b.priority;
2739 		return strcmp(a.name->GetCStr(), b.name->GetCStr()) < 0;
2740 	});
2741 	// Setup group
2742 	properties.name = name;
2743 	if (properties.props.size() != new_properties_resolved.size())
2744 	{
2745 		layout_valid = false;
2746 		properties.props.resize(new_properties_resolved.size());
2747 	}
2748 	C4Effect *fx = target_proplist->GetEffect();
2749 	for (int32_t i = 0; i < new_properties_resolved.size(); ++i)
2750 	{
2751 		Property *prop = &properties.props[i];
2752 		// Property access path
2753 		prop->parent_value.SetPropList(target_proplist);
2754 		prop->property_path = group_target_path;
2755 		// ID for default selection memory
2756 		const PropAndKey &prop_def = new_properties_resolved[i];
2757 		if (default_selection == prop_def.key) *default_selection_index = i;
2758 		// Property data
2759 		prop->help_text = nullptr;
2760 		prop->delegate_info.Set0(); // default C4Value delegate
2761 		prop->group_idx = group_index;
2762 		prop->about_to_edit = false;
2763 		prop->key = prop_def.prop->GetPropertyStr(P_Key);
2764 		if (!prop->key) properties.props[i].key = prop_def.key;
2765 		prop->display_name = prop_def.name;
2766 		if (!prop->display_name) prop->display_name = prop_def.key;
2767 		prop->help_text = prop_def.prop->GetPropertyStr(P_EditorHelp);
2768 		prop->priority = prop_def.priority;
2769 		prop->delegate_info.SetPropList(prop_def.prop);
2770 		prop->delegate = delegate_factory->GetDelegateByValue(prop->delegate_info);
2771 		C4Value v;
2772 		C4Value v_target_proplist = C4VPropList(target_proplist);
2773 		prop->delegate->GetPropertyValue(v_target_proplist, prop->key, 0, &v);
2774 		// Connect editable shape to property
2775 		C4PropertyPath new_shape_property_path = prop->delegate->GetPathForProperty(prop);
2776 		const C4PropertyDelegateShape *new_shape_delegate = prop->delegate->GetShapeDelegate(v, &new_shape_property_path);
2777 		if (new_shape_delegate != prop->shape_delegate || !(prop->shape_property_path == new_shape_property_path))
2778 		{
2779 			prop->shape_delegate = new_shape_delegate;
2780 			prop->shape_property_path = new_shape_property_path;
2781 			if (new_shape_delegate)
2782 			{
2783 				// Re-use loaded shape if possible (e.g. if only the index has moved)
2784 				std::string shape_index = std::string(prop->shape_property_path.GetGetPath());
2785 				prop->shape = &shapes[shape_index];
2786 				C4ConsoleQtShape *shape = prop->shape->Get();
2787 				if (shape)
2788 				{
2789 					if (shape->GetProperties() != new_shape_delegate->GetCreationProps().getPropList())
2790 					{
2791 						// Shape at same path but with different properties? Then re-create
2792 						shape = nullptr;
2793 					}
2794 				}
2795 				if (!shape)
2796 				{
2797 					// New shape or shape type mismatch: Generate new shape at this path and put into the shape holder list
2798 					shape = ::Console.EditCursor.GetShapes()->CreateShape(base_object ? base_object : target_proplist->GetObject(), new_shape_delegate->GetCreationProps().getPropList(), v, new_shape_delegate);
2799 					C4PropertyDelegateFactory *factory = this->delegate_factory;
2800 					new_shape_delegate->ConnectSignals(shape, prop->shape_property_path);
2801 					prop->shape->Set(shape);
2802 					prop->shape->SetLastValue(v);
2803 				}
2804 			}
2805 			else
2806 			{
2807 				prop->shape = nullptr;
2808 			}
2809 		}
2810 		if (prop->shape)
2811 		{
2812 			// Mark this shape to be kept aftre update is complete
2813 			prop->shape->visit();
2814 			// Update shape by value if it was changed externally
2815 			if (!prop->shape->GetLastValue().IsIdenticalTo(v))
2816 			{
2817 				prop->shape->Get()->SetValue(v);
2818 				prop->shape->SetLastValue(v);
2819 			}
2820 		}
2821 	}
2822 	return true;
2823 }
2824 
AddEffectGroup(int32_t group_index,C4Object * base_object)2825 bool C4ConsoleQtPropListModel::AddEffectGroup(int32_t group_index, C4Object *base_object)
2826 {
2827 	// Count non-dead effects
2828 	C4Effect **effect_list = base_object ? &base_object->pEffects : &::ScriptEngine.pGlobalEffects;
2829 	int32_t num_effects = 0;
2830 	for (C4Effect *effect = *effect_list; effect; effect = effect->pNext)
2831 	{
2832 		num_effects += effect->IsActive();
2833 	}
2834 	// Return false to signal that no effect group has been added
2835 	if (!num_effects) return false;
2836 	// Prepare group array
2837 	if (property_groups.size() == group_index)
2838 	{
2839 		layout_valid = false;
2840 		property_groups.resize(group_index + 1);
2841 	}
2842 	PropertyGroup &properties = property_groups[group_index];
2843 	if (properties.props.size() != num_effects)
2844 	{
2845 		layout_valid = false;
2846 		properties.props.resize(num_effects);
2847 	}
2848 	properties.name = LoadResStr("IDS_CNS_EFFECTS");
2849 	// Add all (non-dead) effects of given object (or global effects if base_object is nullptr)
2850 	int32_t num_added = 0;
2851 	for (C4Effect *effect = *effect_list; effect; effect = effect->pNext)
2852 	{
2853 		if (effect->IsActive())
2854 		{
2855 			Property *prop = &properties.props[num_added++];
2856 			prop->parent_value.SetPropList(base_object ? (C4PropList *) base_object : &::ScriptEngine);
2857 			prop->property_path = C4PropertyPath(effect, base_object);
2858 			prop->help_text = nullptr;
2859 			prop->delegate_info.Set0();
2860 			prop->group_idx = group_index;
2861 			prop->key = ::Strings.RegString(prop->property_path.GetGetPath());
2862 			prop->display_name = effect->GetPropertyStr(P_Name);
2863 			prop->priority = 0;
2864 			prop->delegate = delegate_factory->GetEffectDelegate();
2865 			prop->shape = nullptr;
2866 			prop->shape_delegate = nullptr;
2867 			prop->shape_property_path.Clear();
2868 			prop->about_to_edit = false;
2869 			prop->group_idx = group_index;
2870 		}
2871 	}
2872 	// Return true to signal that effect group has been added
2873 	return true;
2874 }
2875 
SetBasePropList(C4PropList * new_proplist)2876 void C4ConsoleQtPropListModel::SetBasePropList(C4PropList *new_proplist)
2877 {
2878 	// Clear stack and select new proplist
2879 	// Update properties
2880 	target_value.SetPropList(new_proplist);
2881 	base_proplist.SetPropList(new_proplist);
2882 	// objects derive their custom properties
2883 	info_proplist.SetPropList(target_value.getObj());
2884 	target_path = C4PropertyPath(new_proplist);
2885 	target_path_stack.clear();
2886 	UpdateValue(true);
2887 	delegate_factory->OnPropListChanged();
2888 }
2889 
DescendPath(const C4Value & new_value,C4PropList * new_info_proplist,const C4PropertyPath & new_path)2890 void C4ConsoleQtPropListModel::DescendPath(const C4Value &new_value, C4PropList *new_info_proplist, const C4PropertyPath &new_path)
2891 {
2892 	// Add previous proplist to stack
2893 	target_path_stack.emplace_back(target_path, target_value, info_proplist);
2894 	// descend
2895 	target_value = new_value;
2896 	info_proplist.SetPropList(new_info_proplist);
2897 	target_path = new_path;
2898 	UpdateValue(true);
2899 	delegate_factory->OnPropListChanged();
2900 }
2901 
AscendPath()2902 void C4ConsoleQtPropListModel::AscendPath()
2903 {
2904 	// Go up in target stack (if possible)
2905 	for (;;)
2906 	{
2907 		if (!target_path_stack.size())
2908 		{
2909 			SetBasePropList(nullptr);
2910 			return;
2911 		}
2912 		TargetStackEntry entry = target_path_stack.back();
2913 		target_path_stack.pop_back();
2914 		if (!entry.value || !entry.info_proplist) continue; // property was removed; go up further in stack
2915 		// Safety: Make sure we're still on the same value
2916 		C4Value target = entry.path.ResolveValue();
2917 		if (!target.IsIdenticalTo(entry.value)) continue;
2918 		// Set new value
2919 		target_path = entry.path;
2920 		target_value = entry.value;
2921 		info_proplist = entry.info_proplist;
2922 		UpdateValue(true);
2923 		break;
2924 	}
2925 	// Any current editor needs to close
2926 	delegate_factory->OnPropListChanged();
2927 }
2928 
UpdateValue(bool select_default)2929 void C4ConsoleQtPropListModel::UpdateValue(bool select_default)
2930 {
2931 	emit layoutAboutToBeChanged();
2932 	// Update target value from path
2933 	target_value = target_path.ResolveValue();
2934 	// Prepare shape list update
2935 	C4ConsoleQtShapeHolder::begin_visit();
2936 	// Safe-get from C4Values in case any prop lists or arrays got deleted
2937 	int32_t num_groups, default_selection_group = -1, default_selection_index = -1;
2938 	switch (target_value.GetType())
2939 	{
2940 	case C4V_PropList:
2941 		num_groups = UpdateValuePropList(target_value._getPropList(), &default_selection_group, &default_selection_index);
2942 		break;
2943 	case C4V_Array:
2944 		num_groups = UpdateValueArray(target_value._getArray(), &default_selection_group, &default_selection_index);
2945 		break;
2946 	default: // probably nil
2947 		num_groups = 0;
2948 		break;
2949 	}
2950 	// Remove any unreferenced shapes
2951 	for (auto iter = shapes.begin(); iter != shapes.end(); )
2952 	{
2953 		if (!iter->second.was_visited())
2954 		{
2955 			iter = shapes.erase(iter);
2956 		}
2957 		else
2958 		{
2959 			++iter;
2960 		}
2961 	}
2962 	// Update model range
2963 	if (num_groups != property_groups.size())
2964 	{
2965 		layout_valid = false;
2966 		property_groups.resize(num_groups);
2967 	}
2968 	if (!layout_valid)
2969 	{
2970 		// We do not adjust persistent indices for now
2971 		// Usually, if layout changed, it's because the target value changed and we don't want to select/expand random stuff in the new proplist
2972 		layout_valid = true;
2973 	}
2974 	emit layoutChanged();
2975 	QModelIndex topLeft = index(0, 0, QModelIndex());
2976 	QModelIndex bottomRight = index(rowCount() - 1, columnCount() - 1, QModelIndex());
2977 	emit dataChanged(topLeft, bottomRight);
2978 	// Initial selection
2979 	if (select_default) emit ProplistChanged(default_selection_group, default_selection_index);
2980 }
2981 
UpdateSelection(int32_t major_sel,int32_t minor_sel) const2982 void C4ConsoleQtPropListModel::UpdateSelection(int32_t major_sel, int32_t minor_sel) const
2983 {
2984 	if (selection_model)
2985 	{
2986 		// Select by indexed elements only
2987 		selection_model->clearSelection();
2988 		if (major_sel >= 0)
2989 		{
2990 			QModelIndex sel = index(major_sel, 0, QModelIndex());
2991 			if (minor_sel >= 0) sel = index(minor_sel, 0, sel);
2992 			selection_model->select(sel, QItemSelectionModel::SelectCurrent);
2993 		}
2994 		else
2995 		{
2996 			selection_model->select(QModelIndex(), QItemSelectionModel::SelectCurrent);
2997 		}
2998 	}
2999 }
3000 
UpdateValuePropList(C4PropList * target_proplist,int32_t * default_selection_group,int32_t * default_selection_index)3001 int32_t C4ConsoleQtPropListModel::UpdateValuePropList(C4PropList *target_proplist, int32_t *default_selection_group, int32_t *default_selection_index)
3002 {
3003 	assert(target_proplist);
3004 	C4PropList *base_proplist = this->base_proplist.getPropList();
3005 	C4Object *base_obj = this->base_proplist.getObj(), *obj = nullptr;
3006 	C4PropList *info_proplist = this->info_proplist.getPropList();
3007 	int32_t num_groups = 0;
3008 	// Selected shape properties
3009 	C4ConsoleQtShape *selected_shape = ::Console.EditCursor.GetShapes()->GetSelectedShape();
3010 	if (selected_shape)
3011 	{
3012 		// Find property information for this shape
3013 		// Could also remember this pointer for every shape holder
3014 		// - but that would have to be updated on any property group vector resize
3015 		Property *prop = nullptr;
3016 		for (PropertyGroup &grp : property_groups)
3017 		{
3018 			for (Property &check_prop : grp.props)
3019 			{
3020 				if (check_prop.shape && check_prop.shape->Get() == selected_shape)
3021 				{
3022 					prop = &check_prop;
3023 					break;
3024 				}
3025 			}
3026 			if (prop) break;
3027 		}
3028 		// Update selected shape item information
3029 		if (prop && prop->delegate)
3030 		{
3031 			C4PropList *shape_item_editorprops, *shape_item_value;
3032 			C4String *shape_item_name = nullptr;
3033 			C4PropertyPath shape_item_target_path;
3034 			C4Value v;
3035 			C4Value v_target_proplist = C4VPropList(target_proplist);
3036 			prop->delegate->GetPropertyValue(v_target_proplist, prop->key, 0, &v);
3037 			C4PropertyPath shape_property_path = prop->delegate->GetPathForProperty(prop);
3038 			const C4PropertyDelegateShape *current_shape_delegate = prop->delegate->GetShapeDelegate(v, &shape_property_path); // to resolve v
3039 			if (::Console.EditCursor.GetShapes()->GetSelectedShapeData(v, prop->shape_property_path, &shape_item_editorprops, &shape_item_value, &shape_item_name, &shape_item_target_path))
3040 			{
3041 				if (AddPropertyGroup(shape_item_editorprops, num_groups, QString(shape_item_name ? shape_item_name->GetCStr() :"???"), shape_item_value, shape_item_target_path, obj, nullptr, nullptr))
3042 				{
3043 					++num_groups;
3044 				}
3045 			}
3046 		}
3047 	}
3048 	// Published properties
3049 	if (info_proplist)
3050 	{
3051 		C4String *default_selection = info_proplist->GetPropertyStr(P_DefaultEditorProp);
3052 		obj = info_proplist->GetObject();
3053 		// Properties from effects (no inheritance supported)
3054 		if (obj)
3055 		{
3056 			for (C4Effect *fx = obj->pEffects; fx; fx = fx->pNext)
3057 			{
3058 				if (!fx->IsActive()) continue; // skip dead effects
3059 				QString name = fx->GetName();
3060 				C4PropList *effect_editorprops = fx->GetPropertyPropList(P_EditorProps);
3061 				if (effect_editorprops && AddPropertyGroup(effect_editorprops, num_groups, name, fx, C4PropertyPath(fx, obj), obj, nullptr, nullptr))
3062 					++num_groups;
3063 			}
3064 		}
3065 		// Properties from object (but not on definition)
3066 		if (obj || !info_proplist->GetDef())
3067 		{
3068 			C4PropList *info_editorprops = info_proplist->GetPropertyPropList(P_EditorProps);
3069 			if (info_editorprops)
3070 			{
3071 				QString name = info_proplist->GetName();
3072 				if (AddPropertyGroup(info_editorprops, num_groups, name, target_proplist, target_path, base_obj, default_selection, default_selection_index))
3073 					++num_groups;
3074 				// Assign group for default selection
3075 				if (*default_selection_index >= 0)
3076 				{
3077 					*default_selection_group = num_groups - 1;
3078 					default_selection = nullptr; // don't find any other instances
3079 				}
3080 			}
3081 		}
3082 		// properties from global list for objects
3083 		if (obj)
3084 		{
3085 			C4Def *editor_base = C4Id2Def(C4ID::EditorBase);
3086 			C4PropList *info_editorprops = nullptr;
3087 
3088 			if (editor_base && (info_editorprops = editor_base->GetPropertyPropList(P_EditorProps)))
3089 			{
3090 				if (AddPropertyGroup(info_editorprops, num_groups, LoadResStr("IDS_CNS_OBJECT"), target_proplist, target_path, base_obj, nullptr, nullptr))
3091 					++num_groups;
3092 			}
3093 		}
3094 	}
3095 	// Always: Internal properties
3096 	auto new_properties = target_proplist->GetSortedLocalProperties();
3097 	if (property_groups.size() == num_groups) property_groups.resize(num_groups + 1);
3098 	PropertyGroup &internal_properties = property_groups[num_groups];
3099 	internal_properties.name = LoadResStr("IDS_CNS_INTERNAL");
3100 	internal_properties.props.resize(new_properties.size());
3101 	for (int32_t i = 0; i < new_properties.size(); ++i)
3102 	{
3103 		internal_properties.props[i].parent_value = this->target_value;
3104 		internal_properties.props[i].property_path = target_path;
3105 		internal_properties.props[i].key = new_properties[i];
3106 		internal_properties.props[i].display_name = new_properties[i];
3107 		internal_properties.props[i].help_text = nullptr;
3108 		internal_properties.props[i].priority = 0;
3109 		internal_properties.props[i].delegate_info.Set0(); // default C4Value delegate
3110 		internal_properties.props[i].delegate = nullptr; // init when needed
3111 		internal_properties.props[i].group_idx = num_groups;
3112 		internal_properties.props[i].shape = nullptr;
3113 		internal_properties.props[i].shape_property_path.Clear();
3114 		internal_properties.props[i].shape_delegate = nullptr;
3115 		internal_properties.props[i].about_to_edit = false;
3116 	}
3117 	++num_groups;
3118 	// Effects
3119 	// Add after internal because the gorup may be added/removed quickly
3120 	if (obj)
3121 	{
3122 		// Object: Show object effects
3123 		if (AddEffectGroup(num_groups, obj))
3124 		{
3125 			++num_groups;
3126 		}
3127 	}
3128 	else if (target_proplist == &::ScriptEngine)
3129 	{
3130 		// Global object: Show global effects
3131 		if (AddEffectGroup(num_groups, nullptr))
3132 		{
3133 			++num_groups;
3134 		}
3135 	}
3136 	return num_groups;
3137 }
3138 
UpdateValueArray(C4ValueArray * target_array,int32_t * default_selection_group,int32_t * default_selection_index)3139 int32_t C4ConsoleQtPropListModel::UpdateValueArray(C4ValueArray *target_array, int32_t *default_selection_group, int32_t *default_selection_index)
3140 {
3141 	if (property_groups.empty())
3142 	{
3143 		layout_valid = false;
3144 		property_groups.resize(1);
3145 	}
3146 	C4PropList *info_proplist = this->info_proplist.getPropList();
3147 	C4Value elements_delegate_value;
3148 	if (info_proplist) info_proplist->GetProperty(P_Elements, &elements_delegate_value);
3149 	property_groups[0].name = (info_proplist ? info_proplist->GetName() : LoadResStr("IDS_CNS_ARRAYEDIT"));
3150 	PropertyGroup &properties = property_groups[0];
3151 	if (properties.props.size() != target_array->GetSize())
3152 	{
3153 		layout_valid = false;
3154 		properties.props.resize(target_array->GetSize());
3155 	}
3156 	C4PropertyDelegate *item_delegate = delegate_factory->GetDelegateByValue(elements_delegate_value);
3157 	for (int32_t i = 0; i < properties.props.size(); ++i)
3158 	{
3159 		Property &prop = properties.props[i];
3160 		prop.property_path = C4PropertyPath(target_path, i);
3161 		prop.parent_value = target_value;
3162 		prop.display_name = ::Strings.RegString(FormatString("%d", (int)i).getData());
3163 		prop.help_text = nullptr;
3164 		prop.key = nullptr;
3165 		prop.priority = 0;
3166 		prop.delegate_info = elements_delegate_value;
3167 		prop.delegate = item_delegate;
3168 		prop.about_to_edit = false;
3169 		prop.group_idx = 0;
3170 		prop.shape = nullptr; // array elements cannot have shapes
3171 		prop.shape_property_path.Clear();
3172 		prop.shape_delegate = nullptr;
3173 	}
3174 	return 1; // one group for the values
3175 }
3176 
DoOnUpdateCall(const C4PropertyPath & updated_path,const C4PropertyDelegate * delegate)3177 void C4ConsoleQtPropListModel::DoOnUpdateCall(const C4PropertyPath &updated_path, const C4PropertyDelegate *delegate)
3178 {
3179 	// If delegate has its own update clalback, perform that on the root
3180 	const char *update_callback = delegate->GetUpdateCallback();
3181 	if (update_callback)
3182 	{
3183 		::Console.EditCursor.EMControl(CID_Script, new C4ControlScript(FormatString("%s->%s(%s)", updated_path.GetRoot(), update_callback, updated_path.GetGetPath()).getData(), 0, false));
3184 	}
3185 	// Do general object property update control
3186 	C4PropList *base_proplist = this->base_proplist.getPropList();
3187 	C4Value q;
3188 	if (base_proplist && base_proplist->GetProperty(P_EditorPropertyChanged, &q))
3189 	{
3190 		::Console.EditCursor.EMControl(CID_Script, new C4ControlScript(FormatString(R"(%s->%s("%s"))", updated_path.GetRoot(), ::Strings.P[P_EditorPropertyChanged].GetCStr(), updated_path.GetGetPath()).getData(), 0, false));
3191 	}
3192 }
3193 
GetPropByIndex(const QModelIndex & index) const3194 C4ConsoleQtPropListModel::Property *C4ConsoleQtPropListModel::GetPropByIndex(const QModelIndex &index) const
3195 {
3196 	if (!index.isValid()) return nullptr;
3197 	// Resolve group and row
3198 	int32_t group_index = index.internalId(), row = index.row();
3199 	// Prop list access: Properties are on 2nd level
3200 	if (!group_index) return nullptr;
3201 	--group_index;
3202 	if (group_index >= property_groups.size()) return nullptr;
3203 	if (row < 0 || row >= property_groups[group_index].props.size()) return nullptr;
3204 	return const_cast<Property *>(&property_groups[group_index].props[row]);
3205 }
3206 
rowCount(const QModelIndex & parent) const3207 int C4ConsoleQtPropListModel::rowCount(const QModelIndex & parent) const
3208 {
3209 	QModelIndex grandparent;
3210 	// Top level: Property groups
3211 	if (!parent.isValid())
3212 	{
3213 		return property_groups.size();
3214 	}
3215 	// Mid level: Descend into property lists
3216 	grandparent = parent.parent();
3217 	if (!grandparent.isValid())
3218 	{
3219 		if (parent.row() >= 0 && parent.row() < property_groups.size())
3220 			return property_groups[parent.row()].props.size();
3221 	}
3222 	return 0; // no 3rd level depth
3223 }
3224 
columnCount(const QModelIndex & parent) const3225 int C4ConsoleQtPropListModel::columnCount(const QModelIndex & parent) const
3226 {
3227 	return 2; // Name + Data (or Index + Data)
3228 }
3229 
headerData(int section,Qt::Orientation orientation,int role) const3230 QVariant C4ConsoleQtPropListModel::headerData(int section, Qt::Orientation orientation, int role) const
3231 {
3232 	// Table headers
3233 	if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal)
3234 	{
3235 		if (section == 0)
3236 			if (target_value.GetType() == C4V_Array)
3237 				return QVariant(LoadResStr("IDS_CNS_INDEXSHORT"));
3238 			else
3239 				return QVariant(LoadResStr("IDS_CTL_NAME"));
3240 		if (section == 1) return QVariant(LoadResStr("IDS_CNS_VALUE"));
3241 	}
3242 	return QVariant();
3243 }
3244 
data(const QModelIndex & index,int role) const3245 QVariant C4ConsoleQtPropListModel::data(const QModelIndex & index, int role) const
3246 {
3247 	// Headers
3248 	int32_t group_index = index.internalId();
3249 	if (!group_index)
3250 	{
3251 		if (!index.column())
3252 		{
3253 			if (role == Qt::DisplayRole)
3254 			{
3255 				if (index.row() >= 0 && index.row() < property_groups.size())
3256 					return property_groups[index.row()].name;
3257 			}
3258 			else if (role == Qt::FontRole)
3259 			{
3260 				return header_font;
3261 			}
3262 		}
3263 		return QVariant();
3264 	}
3265 	// Query latest data from prop list
3266 	Property *prop = GetPropByIndex(index);
3267 	if (!prop) return QVariant();
3268 	if (!prop->delegate) prop->delegate = delegate_factory->GetDelegateByValue(prop->delegate_info);
3269 	if (role == Qt::DisplayRole)
3270 	{
3271 		switch (index.column())
3272 		{
3273 		case 0: // First col: Property Name
3274 			return QVariant(prop->display_name->GetCStr());
3275 		case 1: // Second col: Property value
3276 		{
3277 			C4Value v;
3278 			prop->delegate->GetPropertyValue(prop->parent_value, prop->key, index.row(), &v);
3279 			return QVariant(prop->delegate->GetDisplayString(v, target_value.getObj(), true));
3280 		}
3281 		}
3282 	}
3283 	else if (role == Qt::BackgroundColorRole && index.column()==1)
3284 	{
3285 		C4Value v;
3286 		prop->delegate->GetPropertyValue(prop->parent_value, prop->key, index.row(), &v);
3287 		QColor bgclr = prop->delegate->GetDisplayBackgroundColor(v, target_value.getObj());
3288 		if (bgclr.isValid()) return bgclr;
3289 	}
3290 	else if (role == Qt::TextColorRole && index.column() == 1)
3291 	{
3292 		C4Value v;
3293 		prop->delegate->GetPropertyValue(prop->parent_value, prop->key, index.row(), &v);
3294 		QColor txtclr = prop->delegate->GetDisplayTextColor(v, target_value.getObj());
3295 		if (txtclr.isValid()) return txtclr;
3296 	}
3297 	else if (role == Qt::DecorationRole && index.column() == 0 && prop->help_text && Config.Developer.ShowHelp)
3298 	{
3299 		// Help icons in left column
3300 		return QIcon(":/editor/res/Help.png");
3301 	}
3302 	else if (role == Qt::FontRole && index.column() == 0)
3303 	{
3304 		if (prop->priority >= 100) return important_property_font;
3305 	}
3306 	else if (role == Qt::ToolTipRole && index.column() == 0)
3307 	{
3308 		// Tooltip from property description. Default to display name in case it got truncated.
3309 		if (prop->help_text)
3310 			return QString(prop->help_text->GetCStr());
3311 		else
3312 			return QString(prop->display_name->GetCStr());
3313 	}
3314 	// Nothing to show
3315 	return QVariant();
3316 }
3317 
index(int row,int column,const QModelIndex & parent) const3318 QModelIndex C4ConsoleQtPropListModel::index(int row, int column, const QModelIndex &parent) const
3319 {
3320 	if (column < 0 || column > 1) return QModelIndex();
3321 	// Top level index?
3322 	if (!parent.isValid())
3323 	{
3324 		// Top level has headers only
3325 		if (row < 0 || row >= property_groups.size()) return QModelIndex();
3326 		return createIndex(row, column, (quintptr)0u);
3327 	}
3328 	if (parent.internalId()) return QModelIndex(); // No 3rd level depth
3329 	// Validate range of property
3330 	const PropertyGroup *property_group = nullptr;
3331 	if (parent.row() >= 0 && parent.row() < property_groups.size())
3332 	{
3333 		property_group = &property_groups[parent.row()];
3334 		if (row < 0 || row >= property_group->props.size()) return QModelIndex();
3335 		return createIndex(row, column, (quintptr)parent.row()+1);
3336 	}
3337 	return QModelIndex();
3338 }
3339 
parent(const QModelIndex & index) const3340 QModelIndex C4ConsoleQtPropListModel::parent(const QModelIndex &index) const
3341 {
3342 	// Parent: Stored in internal ID
3343 	auto parent_idx = index.internalId();
3344 	if (parent_idx) return createIndex(parent_idx - 1, 0, (quintptr)0u);
3345 	return QModelIndex();
3346 }
3347 
flags(const QModelIndex & index) const3348 Qt::ItemFlags C4ConsoleQtPropListModel::flags(const QModelIndex &index) const
3349 {
3350 	Qt::ItemFlags flags = QAbstractItemModel::flags(index) | Qt::ItemIsDropEnabled;
3351 	Property *prop = GetPropByIndex(index);
3352 	if (index.isValid() && prop)
3353 	{
3354 		flags &= ~Qt::ItemIsDropEnabled; // only drop between the lines
3355 		if (index.column() == 0)
3356 		{
3357 			// array elements can be re-arranged
3358 			if (prop->parent_value.GetType() == C4V_Array) flags |= Qt::ItemIsDragEnabled;
3359 		}
3360 		else if (index.column() == 1)
3361 		{
3362 			// Disallow editing on readonly target (e.g. frozen proplist).
3363 			// But always allow editing of effects.
3364 			bool readonly = IsTargetReadonly() && prop->delegate != delegate_factory->GetEffectDelegate();
3365 			if (!readonly)
3366 				flags |= Qt::ItemIsEditable;
3367 			else
3368 				flags &= ~Qt::ItemIsEnabled;
3369 		}
3370 	}
3371 	return flags;
3372 }
3373 
supportedDropActions() const3374 Qt::DropActions C4ConsoleQtPropListModel::supportedDropActions() const
3375 {
3376 	return Qt::MoveAction;
3377 }
3378 
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)3379 bool C4ConsoleQtPropListModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
3380 {
3381 	// Drag+Drop movement on array only
3382 	if (action != Qt::MoveAction) return false;
3383 	C4ValueArray *arr = target_value.getArray();
3384 	if (!arr) return false;
3385 	if (!data->hasFormat("application/vnd.text")) return false;
3386 	if (row < 0) return false; // outside range: Could be above or below. Better don't drag at all.
3387 	if (!parent.isValid()) return false; // in array only
3388 	// Decode indices of rows to move
3389 	QByteArray encodedData = data->data("application/vnd.text");
3390 	StdStrBuf rearrange_call;
3391 	rearrange_call.Format("MoveArrayItems(%%s, [%s], %d)", encodedData.constData(), row);
3392 	target_path.DoCall(rearrange_call.getData());
3393 	return true;
3394 }
3395 
mimeTypes() const3396 QStringList C4ConsoleQtPropListModel::mimeTypes() const
3397 {
3398 	QStringList types;
3399 	types << "application/vnd.text";
3400 	return types;
3401 }
3402 
mimeData(const QModelIndexList & indexes) const3403 QMimeData *C4ConsoleQtPropListModel::mimeData(const QModelIndexList &indexes) const
3404 {
3405 	// Add all moved indexes
3406 	QMimeData *mimeData = new QMimeData();
3407 	QByteArray encodedData;
3408 	int32_t count = 0;
3409 	for (const QModelIndex &index : indexes)
3410 	{
3411 		if (index.isValid() && index.internalId())
3412 		{
3413 			if (count) encodedData.append(",");
3414 			encodedData.append(QString::number(index.row()));
3415 			++count;
3416 		}
3417 	}
3418 	mimeData->setData("application/vnd.text", encodedData);
3419 	return mimeData;
3420 }
3421 
GetTargetPathHelp() const3422 QString C4ConsoleQtPropListModel::GetTargetPathHelp() const
3423 {
3424 	// Help text in EditorInfo prop. Fall back to description.
3425 	C4PropList *info_proplist = this->info_proplist.getPropList();
3426 	if (!info_proplist) return QString();
3427 	C4String *desc = info_proplist->GetPropertyStr(P_EditorHelp);
3428 	if (!desc) desc = info_proplist->GetPropertyStr(P_Description);
3429 	if (!desc) return QString();
3430 	QString result = QString(desc->GetCStr());
3431 	result = result.replace('|', '\n');
3432 	return result;
3433 }
3434 
GetTargetPathName() const3435 const char *C4ConsoleQtPropListModel::GetTargetPathName() const
3436 {
3437 	// Name prop of current info.
3438 	C4PropList *info_proplist = this->info_proplist.getPropList();
3439 	if (!info_proplist) return nullptr;
3440 	C4String *name = info_proplist->GetPropertyStr(P_Name);
3441 	return name ? name->GetCStr() : nullptr;
3442 }
3443 
AddArrayElement()3444 void C4ConsoleQtPropListModel::AddArrayElement()
3445 {
3446 	C4Value new_val;
3447 	C4PropList *info_proplist = this->info_proplist.getPropList();
3448 	C4PropListStatic *info_proplist_static = nullptr;
3449 	if (info_proplist)
3450 	{
3451 		info_proplist->GetProperty(P_DefaultValue, &new_val);
3452 		info_proplist_static = info_proplist->IsStatic();
3453 	}
3454 	target_path.DoCall(FormatString("PushBack(%%s, %s)", new_val.GetDataString(10, info_proplist_static).getData()).getData());
3455 }
3456 
RemoveArrayElement()3457 void C4ConsoleQtPropListModel::RemoveArrayElement()
3458 {
3459 	// Compose script command to remove all selected array indices
3460 	StdStrBuf script;
3461 	for (QModelIndex idx : selection_model->selectedIndexes())
3462 		if (idx.isValid() && idx.column() == 0)
3463 			if (script.getLength())
3464 				script.AppendFormat(",%d", idx.row());
3465 			else
3466 				script.AppendFormat("%d", idx.row());
3467 	if (script.getLength()) target_path.DoCall(FormatString("RemoveArrayIndices(%%s, [%s])", script.getData()).getData());
3468 }
3469 
IsTargetReadonly() const3470 bool C4ConsoleQtPropListModel::IsTargetReadonly() const
3471 {
3472 	if (target_path.IsEmpty()) return true;
3473 	switch (target_value.GetType())
3474 	{
3475 	case C4V_Array:
3476 		// Arrays are never frozen
3477 		return false;
3478 	case C4V_PropList:
3479 	{
3480 		C4PropList *parent_proplist = target_value._getPropList();
3481 		if (parent_proplist->IsFrozen()) return true;
3482 		return false;
3483 	}
3484 	default:
3485 		return true;
3486 	}
3487 }
3488 
GetShapeByPropertyPath(const char * property_path)3489 class C4ConsoleQtShape *C4ConsoleQtPropListModel::GetShapeByPropertyPath(const char *property_path)
3490 {
3491 	// Lookup in map
3492 	auto entry = shapes.find(std::string(property_path));
3493 	if (entry == shapes.end()) return nullptr;
3494 	return entry->second.Get();
3495 }
3496