1 /*
2  * OpenClonk, http://www.openclonk.org
3  *
4  * Copyright (c) 2014-2016, The OpenClonk Team and contributors
5  *
6  * Distributed under the terms of the ISC license; see accompanying file
7  * "COPYING" for details.
8  *
9  * "Clonk" is a registered trademark of Matthes Bender, used with permission.
10  * See accompanying file "TRADEMARK" for details.
11  *
12  * To redistribute this file separately, substitute the full license texts
13  * for the above references.
14  */
15 
16  /*
17 	A flexible ingame menu system that can be used to compose large GUIs out of multiple windows.
18 
19 	Every window is basically a rectangle that can contain some make-up-information (symbol/text/...) and coordinates.
20 	Those coordinates can either be relative to the window's parent or in total pixels or a mixture of both.
21 
22 	The entry point for all of the callbacks for mouse input, drawing, etc. is one normal window which always exists and happens
23 	to be the parent of ALL of the script-created menus. Callbacks are usually forwarded to the children.
24 
25 	If you want to add new window properties (similar to backgroundColor, onClickAction etc.) you have to make sure that they are
26 	serialized correctly and cleaned up if necessary when a menu window is closed or the property is overwritten by a script call!
27 */
28 
29 #include "C4Include.h"
30 #include "gui/C4ScriptGuiWindow.h"
31 
32 #include "control/C4Control.h"
33 #include "game/C4Application.h"
34 #include "game/C4GraphicsSystem.h"
35 #include "game/C4Viewport.h"
36 #include "graphics/C4Draw.h"
37 #include "graphics/C4GraphicsResource.h"
38 #include "gui/C4MouseControl.h"
39 #include "lib/StdColors.h"
40 #include "object/C4Def.h"
41 #include "object/C4DefList.h"
42 #include "object/C4Object.h"
43 #include "player/C4Player.h"
44 #include "player/C4PlayerList.h"
45 
46 // Adds some helpful logs for hunting control & menu based desyncs.
47 //#define MenuDebugLogF(...) DebugLogF(__VA_ARGS__)
48 #define MenuDebugLogF(...) ((void)0)
49 
50 // This in in EM! Also, golden ratio
51 const float C4ScriptGuiWindow::standardWidth = 50.0f;
52 const float C4ScriptGuiWindow::standardHeight = 31.0f;
53 
Em2Pix(float em)54 float C4ScriptGuiWindow::Em2Pix(float em)
55 {
56 	return static_cast<float>(::GraphicsResource.FontRegular.GetFontHeight()) * em;
57 }
58 
Pix2Em(float pix)59 float C4ScriptGuiWindow::Pix2Em(float pix)
60 {
61 	return pix / static_cast<float>(std::max<int32_t>(1, ::GraphicsResource.FontRegular.GetFontHeight()));
62 }
63 
~C4ScriptGuiWindowAction()64 C4ScriptGuiWindowAction::~C4ScriptGuiWindowAction()
65 {
66 	if (text)
67 		text->DecRef();
68 	if (nextAction)
69 		delete nextAction;
70 }
71 
ToC4Value(bool first)72 const C4Value C4ScriptGuiWindowAction::ToC4Value(bool first)
73 {
74 	C4ValueArray *array = new C4ValueArray();
75 
76 	switch (action)
77 	{
78 	case C4ScriptGuiWindowActionID::Call:
79 		array->SetSize(4);
80 		array->SetItem(0, C4Value(action));
81 		array->SetItem(1, C4Value(target));
82 		array->SetItem(2, C4Value(text));
83 		array->SetItem(3, value);
84 		break;
85 
86 	case C4ScriptGuiWindowActionID::SetTag:
87 		array->SetSize(4);
88 		array->SetItem(0, C4Value(action));
89 		array->SetItem(1, C4Value(text));
90 		array->SetItem(2, C4Value(subwindowID));
91 		array->SetItem(3, C4Value(target));
92 		break;
93 
94 	case 0: // can actually happen if the action is invalidated
95 		break;
96 
97 	default:
98 		assert(false && "trying to save C4ScriptGuiWindowAction without valid action");
99 		break;
100 	}
101 
102 	assert (array->GetSize() < 6);
103 	array->SetSize(6);
104 	array->SetItem(5, C4Value(id));
105 
106 	if (!first || !nextAction) return C4Value(array);
107 
108 	// this action is the first in a chain of actions
109 	// all following actions (and this one) have to be put into another array
110 	C4ValueArray *container = new C4ValueArray();
111 	int32_t size = 1;
112 	container->SetSize(size);
113 	container->SetItem(0, C4Value(array));
114 
115 	C4ScriptGuiWindowAction *next = nextAction;
116 	while (next)
117 	{
118 		C4Value val = next->ToC4Value(false);
119 		++size;
120 		container->SetSize(size);
121 		container->SetItem(size - 1, val);
122 		next = next->nextAction;
123 	}
124 	return C4Value(container);
125 }
126 
ClearPointers(C4Object * pObj)127 void C4ScriptGuiWindowAction::ClearPointers(C4Object *pObj)
128 {
129 	C4Object *targetObj = target ? target->GetObject() : nullptr;
130 
131 	if (targetObj == pObj)
132 	{
133 		// not only forget object, but completely invalidate action
134 		action = 0;
135 		target = nullptr;
136 	}
137 	if (nextAction)
138 		nextAction->ClearPointers(pObj);
139 }
Init(C4ValueArray * array,int32_t index)140 bool C4ScriptGuiWindowAction::Init(C4ValueArray *array, int32_t index)
141 {
142 	if (array->GetSize() == 0) // safety
143 		return false;
144 
145 	// an array of actions?
146 	if (array->GetItem(0).getArray())
147 	{
148 		// add action to action chain?
149 		if (index+1 < array->GetSize())
150 		{
151 			nextAction = new C4ScriptGuiWindowAction();
152 			nextAction->Init(array, index + 1);
153 		}
154 		// continue with one sub array
155 		array = array->GetItem(index).getArray();
156 		if (!array) return false;
157 	}
158 	// retrieve type of action
159 	int newAction = array->GetItem(0).getInt();
160 	action = 0; // still invalid!
161 
162 	// when loading, the array has a size of 6 with the 5th element being the ID
163 	if (array->GetSize() == 6)
164 		id = array->GetItem(3).getInt();
165 
166 	switch (newAction)
167 	{
168 	case C4ScriptGuiWindowActionID::Call:
169 		if (array->GetSize() < 3) return false;
170 		target = array->GetItem(1).getPropList();
171 		text = array->GetItem(2).getStr();
172 		if (!target || !text) return false;
173 		if (array->GetSize() >= 4)
174 			value = C4Value(array->GetItem(3));
175 		text->IncRef();
176 
177 		// important! needed to identify actions later!
178 		if (!id)
179 		{
180 			id = ::Game.ScriptGuiRoot->GenerateActionID();
181 			MenuDebugLogF("assigning action ID %d\t\taction:%d, text:%s", id, newAction, text->GetCStr());
182 		}
183 
184 		break;
185 
186 	case C4ScriptGuiWindowActionID::SetTag:
187 		if (array->GetSize() < 4) return false;
188 		text = array->GetItem(1).getStr();
189 		if (!text) return false;
190 		text->IncRef();
191 		subwindowID = array->GetItem(2).getInt();
192 		target = array->GetItem(3).getObj(); // getObj on purpose. Need to validate that.
193 		break;
194 
195 	default:
196 		return false;
197 	}
198 
199 	action = newAction;
200 	return true;
201 }
202 
Execute(C4ScriptGuiWindow * parent,int32_t player,int32_t actionType)203 void C4ScriptGuiWindowAction::Execute(C4ScriptGuiWindow *parent, int32_t player, int32_t actionType)
204 {
205 	assert(parent && "C4ScriptGuiWindow::Execute must always be called with parent");
206 	MenuDebugLogF("Excuting action (nextAction: %x, subwID: %d, target: %x, text: %s, type: %d)", nextAction, subwindowID, target, text->GetCStr(), actionType);
207 
208 	// invalid ID? can be set by removal of target object
209 	if (action)
210 	{
211 		// get menu main window
212 		C4ScriptGuiWindow *main = parent;
213 		C4ScriptGuiWindow *from = main;
214 		while (!from->IsRoot())
215 		{
216 			main = from;
217 			from = static_cast<C4ScriptGuiWindow*>(from->GetParent());
218 		}
219 
220 		switch (action)
221 		{
222 		case C4ScriptGuiWindowActionID::Call:
223 		{
224 			if (!target) // ohject removed in the meantime?
225 				break;
226 			MenuDebugLogF("[ACTION REQUEST] action /call/");
227 			// the action needs to be synchronized! Assemble command and put it into control queue!
228 			Game.Input.Add(CID_MenuCommand, new C4ControlMenuCommand(id, player, main->GetID(), parent->GetID(), parent->target, actionType));
229 			break;
230 		}
231 
232 		case C4ScriptGuiWindowActionID::SetTag:
233 		{
234 			C4ScriptGuiWindow *window = main;
235 			if (subwindowID == 0)
236 				window = parent;
237 			else if (subwindowID > 0)
238 			{
239 				C4Object *targetObj = dynamic_cast<C4Object*> (target);
240 				window = main->GetSubWindow(subwindowID, targetObj);
241 			}
242 			if (window)
243 				window->SetTag(text);
244 			break;
245 		}
246 
247 		default:
248 			assert(false && "C4ScriptGuiWindowAction without valid or invalidated ID");
249 			break;
250 		}
251 	} // action
252 
253 	if (nextAction)
254 	{
255 		nextAction->Execute(parent, player, actionType);
256 	}
257 }
258 
ExecuteCommand(int32_t actionID,C4ScriptGuiWindow * parent,int32_t player)259 bool C4ScriptGuiWindowAction::ExecuteCommand(int32_t actionID, C4ScriptGuiWindow *parent, int32_t player)
260 {
261 	MenuDebugLogF("checking action %d (==%d?)\t\tmy action: %d", id, actionID, action);
262 	// target has already been checked for validity
263 	if (id == actionID && action)
264 	{
265 		assert(action == C4ScriptGuiWindowActionID::Call && "C4ControlMenuCommand for invalid action!");
266 
267 		// get menu main window
268 		C4ScriptGuiWindow *main = parent;
269 		C4ScriptGuiWindow *from = main;
270 		while (!from->IsRoot())
271 		{
272 			main = from;
273 			from = static_cast<C4ScriptGuiWindow*>(from->GetParent());
274 		}
275 		MenuDebugLogF("command synced.. target: %x, targetObj: %x, func: %s", target, target->GetObject(), text->GetCStr());
276 		C4AulParSet Pars(value, C4VInt(player), C4VInt(main->GetID()), C4VInt(parent->GetID()), (parent->target && parent->target->Status) ? C4VObj(parent->target) : C4VNull);
277 		target->Call(text->GetCStr(), &Pars);
278 		return true;
279 	}
280 	if (nextAction)
281 		return nextAction->ExecuteCommand(actionID, parent, player);
282 	return false;
283 }
284 
~C4ScriptGuiWindowProperty()285 C4ScriptGuiWindowProperty::~C4ScriptGuiWindowProperty()
286 {
287 	// is cleaned up from destructor of C4ScriptGuiWindow
288 }
289 
SetInt(int32_t to,C4String * tag)290 void C4ScriptGuiWindowProperty::SetInt(int32_t to, C4String *tag)
291 {
292 	if (!tag) tag = &Strings.P[P_Std];
293 	taggedProperties[tag] = Prop();
294 	current = &taggedProperties[tag];
295 	current->d = to;
296 }
SetFloat(float to,C4String * tag)297 void C4ScriptGuiWindowProperty::SetFloat(float to, C4String *tag)
298 {
299 	if (!tag) tag = &Strings.P[P_Std];
300 	taggedProperties[tag] = Prop();
301 	current = &taggedProperties[tag];
302 	current->f = to;
303 }
SetNull(C4String * tag)304 void C4ScriptGuiWindowProperty::SetNull(C4String *tag)
305 {
306 	if (!tag) tag = &Strings.P[P_Std];
307 	taggedProperties[tag] = Prop();
308 	current = &taggedProperties[tag];
309 	current->data = nullptr;
310 }
311 
CleanUp(Prop & prop)312 void C4ScriptGuiWindowProperty::CleanUp(Prop &prop)
313 {
314 	switch (type)
315 	{
316 	case C4ScriptGuiWindowPropertyName::frameDecoration:
317 		if (prop.deco) delete prop.deco;
318 		break;
319 	case C4ScriptGuiWindowPropertyName::onClickAction:
320 	case C4ScriptGuiWindowPropertyName::onMouseInAction:
321 	case C4ScriptGuiWindowPropertyName::onMouseOutAction:
322 	case C4ScriptGuiWindowPropertyName::onCloseAction:
323 		if (prop.action) delete prop.action;
324 		break;
325 	case C4ScriptGuiWindowPropertyName::text:
326 	case C4ScriptGuiWindowPropertyName::tooltip:
327 	case C4ScriptGuiWindowPropertyName::symbolGraphicsName:
328 		if (prop.strBuf) delete prop.strBuf;
329 		break;
330 	default:
331 		break;
332 	}
333 }
334 
CleanUpAll()335 void C4ScriptGuiWindowProperty::CleanUpAll()
336 {
337 	for (auto & taggedProperty : taggedProperties)
338 	{
339 		CleanUp(taggedProperty.second);
340 		if (taggedProperty.first != &Strings.P[P_Std])
341 			taggedProperty.first->DecRef();
342 	}
343 }
344 
ToC4Value()345 const C4Value C4ScriptGuiWindowProperty::ToC4Value()
346 {
347 	C4PropList *proplist = nullptr;
348 
349 	bool onlyOneTag = taggedProperties.size() == 1;
350 	if (!onlyOneTag) // we will need a tagged proplist
351 		proplist = C4PropList::New();
352 
353 	// go through all of the tagged properties and add a property to the proplist containing both the tag name
354 	// and the serialzed C4Value of the properties' value
355 	for(auto & taggedProperty : taggedProperties)
356 	{
357 		C4String *tagString = taggedProperty.first;
358 		const Prop &prop = taggedProperty.second;
359 
360 		C4Value val;
361 
362 		// get value to save
363 		switch (type)
364 		{
365 		case C4ScriptGuiWindowPropertyName::left:
366 		case C4ScriptGuiWindowPropertyName::right:
367 		case C4ScriptGuiWindowPropertyName::top:
368 		case C4ScriptGuiWindowPropertyName::bottom:
369 		case C4ScriptGuiWindowPropertyName::relLeft:
370 		case C4ScriptGuiWindowPropertyName::relRight:
371 		case C4ScriptGuiWindowPropertyName::relTop:
372 		case C4ScriptGuiWindowPropertyName::relBottom:
373 		case C4ScriptGuiWindowPropertyName::leftMargin:
374 		case C4ScriptGuiWindowPropertyName::rightMargin:
375 		case C4ScriptGuiWindowPropertyName::topMargin:
376 		case C4ScriptGuiWindowPropertyName::bottomMargin:
377 		case C4ScriptGuiWindowPropertyName::relLeftMargin:
378 		case C4ScriptGuiWindowPropertyName::relRightMargin:
379 		case C4ScriptGuiWindowPropertyName::relTopMargin:
380 		case C4ScriptGuiWindowPropertyName::relBottomMargin:
381 			assert (false && "Trying to get a single positional value from a GuiWindow for saving. Those should always be saved in pairs of two in a string.");
382 			break;
383 
384 		case C4ScriptGuiWindowPropertyName::backgroundColor:
385 		case C4ScriptGuiWindowPropertyName::style:
386 		case C4ScriptGuiWindowPropertyName::priority:
387 		case C4ScriptGuiWindowPropertyName::player:
388 			val = C4Value(prop.d);
389 			break;
390 
391 		case C4ScriptGuiWindowPropertyName::symbolObject:
392 			val = C4Value(prop.obj);
393 			break;
394 
395 		case C4ScriptGuiWindowPropertyName::symbolDef:
396 			val = C4Value(prop.def);
397 			break;
398 
399 		case C4ScriptGuiWindowPropertyName::frameDecoration:
400 			if (prop.deco)
401 				val = C4Value(prop.deco->pSourceDef);
402 			break;
403 
404 		case C4ScriptGuiWindowPropertyName::text:
405 		case C4ScriptGuiWindowPropertyName::symbolGraphicsName:
406 		case C4ScriptGuiWindowPropertyName::tooltip:
407 		{
408 			if (prop.strBuf)
409 			{
410 				// string existing?
411 				C4String *s = Strings.FindString(prop.strBuf->getData());
412 				if (!s) s = Strings.RegString(prop.strBuf->getData());
413 				val = C4Value(s);
414 			}
415 			break;
416 		}
417 
418 		case C4ScriptGuiWindowPropertyName::onClickAction:
419 		case C4ScriptGuiWindowPropertyName::onMouseInAction:
420 		case C4ScriptGuiWindowPropertyName::onMouseOutAction:
421 		case C4ScriptGuiWindowPropertyName::onCloseAction:
422 			if (prop.action)
423 				val = prop.action->ToC4Value();
424 			break;
425 
426 		default:
427 			assert(false && "C4ScriptGuiWindowAction should never have undefined type");
428 			break;
429 		} // switch
430 
431 		if (onlyOneTag) return val;
432 		assert(proplist);
433 		proplist->SetPropertyByS(tagString, val);
434 	}
435 
436 	return C4Value(proplist);
437 }
438 
Set(const C4Value & value,C4String * tag)439 void C4ScriptGuiWindowProperty::Set(const C4Value &value, C4String *tag)
440 {
441 	C4PropList *proplist = value.getPropList();
442 	bool isTaggedPropList = false;
443 	if (proplist)
444 		isTaggedPropList = !(proplist->GetDef() || proplist->GetObject());
445 
446 	if (isTaggedPropList)
447 	{
448 		std::unique_ptr<C4ValueArray> properties(proplist->GetProperties());
449 		properties->SortStrings();
450 		for (int32_t i = 0; i < properties->GetSize(); ++i)
451 		{
452 			const C4Value &entry = properties->GetItem(i);
453 			C4String *key = entry.getStr();
454 			assert(key && "Proplist returns non-string as key");
455 
456 			C4Value property;
457 			proplist->GetPropertyByS(key, &property);
458 			Set(property, key);
459 		}
460 		return;
461 	}
462 
463 	// special treatment for some that have to be deleted (due to owning string/frame deco/...)
464 	if (taggedProperties.count(tag))
465 		CleanUp(taggedProperties[tag]);
466 	else // new tag, retain the proplist if not standard
467 		if (tag != &Strings.P[P_Std])
468 			tag->IncRef();
469 
470 	taggedProperties[tag] = Prop();
471 	// in order to make /current/ sane, always reset it - not relying on implementation details of std::map
472 	// if the user wants a special tag selected, he should do that (standard selection will still be "Std")
473 	current = &taggedProperties[tag];
474 	currentTag = tag;
475 
476 
477 	// now that a new property entry has been created and the old has been cleaned up, get the data from the C4Value
478 	switch (type)
479 	{
480 	case C4ScriptGuiWindowPropertyName::left:
481 	case C4ScriptGuiWindowPropertyName::right:
482 	case C4ScriptGuiWindowPropertyName::top:
483 	case C4ScriptGuiWindowPropertyName::bottom:
484 	case C4ScriptGuiWindowPropertyName::relLeft:
485 	case C4ScriptGuiWindowPropertyName::relRight:
486 	case C4ScriptGuiWindowPropertyName::relTop:
487 	case C4ScriptGuiWindowPropertyName::relBottom:
488 	case C4ScriptGuiWindowPropertyName::leftMargin:
489 	case C4ScriptGuiWindowPropertyName::rightMargin:
490 	case C4ScriptGuiWindowPropertyName::topMargin:
491 	case C4ScriptGuiWindowPropertyName::bottomMargin:
492 	case C4ScriptGuiWindowPropertyName::relLeftMargin:
493 	case C4ScriptGuiWindowPropertyName::relRightMargin:
494 	case C4ScriptGuiWindowPropertyName::relTopMargin:
495 	case C4ScriptGuiWindowPropertyName::relBottomMargin:
496 		assert (false && "Trying to set positional properties directly. Those should always come parsed from a string.");
497 		break;
498 
499 	case C4ScriptGuiWindowPropertyName::backgroundColor:
500 	case C4ScriptGuiWindowPropertyName::style:
501 	case C4ScriptGuiWindowPropertyName::priority:
502 		current->d = value.getInt();
503 		break;
504 
505 	case C4ScriptGuiWindowPropertyName::player:
506 		if (value == C4VNull)
507 			current->d = ANY_OWNER;
508 		else
509 			current->d = value.getInt();
510 		break;
511 
512 	case C4ScriptGuiWindowPropertyName::symbolObject:
513 	{
514 		C4PropList *symbol = value.getPropList();
515 		if (symbol)
516 			current->obj = symbol->GetObject();
517 		else current->obj = nullptr;
518 		break;
519 	}
520 	case C4ScriptGuiWindowPropertyName::symbolDef:
521 	{
522 		C4PropList *symbol = value.getPropList();
523 		if (symbol)
524 			current->def = symbol->GetDef();
525 		else current->def = nullptr;
526 		break;
527 	}
528 	case C4ScriptGuiWindowPropertyName::frameDecoration:
529 	{
530 		C4Def *def = value.getDef();
531 
532 		if (def)
533 		{
534 			current->deco = new C4GUI::FrameDecoration();
535 			if (!current->deco->SetByDef(def))
536 			{
537 				delete current->deco;
538 				current->deco = nullptr;
539 			}
540 		}
541 		break;
542 	}
543 	case C4ScriptGuiWindowPropertyName::text:
544 	case C4ScriptGuiWindowPropertyName::symbolGraphicsName:
545 	case C4ScriptGuiWindowPropertyName::tooltip:
546 	{
547 		C4String *string = value.getStr();
548 		StdCopyStrBuf *buf = new StdCopyStrBuf();
549 		if (string)
550 			buf->Copy(string->GetCStr());
551 		else buf->Copy("");
552 		current->strBuf = buf;
553 		break;
554 	}
555 	case C4ScriptGuiWindowPropertyName::onClickAction:
556 	case C4ScriptGuiWindowPropertyName::onMouseInAction:
557 	case C4ScriptGuiWindowPropertyName::onMouseOutAction:
558 	case C4ScriptGuiWindowPropertyName::onCloseAction:
559 	{
560 		C4ValueArray *array = value.getArray();
561 		if (array)
562 		{
563 			assert (!current->action && "Prop() contains action prior to assignment");
564 			current->action = new C4ScriptGuiWindowAction();
565 			current->action->Init(array);
566 		}
567 		break;
568 	}
569 
570 	default:
571 		assert(false && "C4ScriptGuiWindowAction should never have undefined type");
572 		break;
573 	} // switch
574 }
575 
ClearPointers(C4Object * pObj)576 void C4ScriptGuiWindowProperty::ClearPointers(C4Object *pObj)
577 {
578 	// assume that we actually contain an object
579 	// go through all the tags and, in case the tag has anything to do with objects, check and clear it
580 	for (auto & taggedProperty : taggedProperties)
581 	{
582 		switch (type)
583 		{
584 		case C4ScriptGuiWindowPropertyName::symbolObject:
585 			if (taggedProperty.second.obj == pObj)
586 				taggedProperty.second.obj = nullptr;
587 		break;
588 
589 		case C4ScriptGuiWindowPropertyName::onClickAction:
590 		case C4ScriptGuiWindowPropertyName::onMouseInAction:
591 		case C4ScriptGuiWindowPropertyName::onMouseOutAction:
592 		case C4ScriptGuiWindowPropertyName::onCloseAction:
593 			if (taggedProperty.second.action)
594 				taggedProperty.second.action->ClearPointers(pObj);
595 		break;
596 		default:
597 			return;
598 		}
599 	}
600 }
601 
SwitchTag(C4String * tag)602 bool C4ScriptGuiWindowProperty::SwitchTag(C4String *tag)
603 {
604 	if (!taggedProperties.count(tag)) return false; // tag not available
605 	if (current == &taggedProperties[tag]) return false; // tag already set?
606 	current = &taggedProperties[tag];
607 	currentTag = tag;
608 	return true;
609 }
610 
GetAllActions()611 std::list<C4ScriptGuiWindowAction*> C4ScriptGuiWindowProperty::GetAllActions()
612 {
613 	std::list<C4ScriptGuiWindowAction*> allActions;
614 	for (auto & taggedProperty : taggedProperties)
615 	{
616 		Prop &p = taggedProperty.second;
617 		if (p.action)
618 			allActions.push_back(p.action);
619 	}
620 	return allActions;
621 }
622 
623 
C4ScriptGuiWindow()624 C4ScriptGuiWindow::C4ScriptGuiWindow() : C4GUI::ScrollWindow(this)
625 {
626 	Init();
627 }
628 
Init()629 void C4ScriptGuiWindow::Init()
630 {
631 	id = 0;
632 	name = nullptr;
633 
634 	isMainWindow = false;
635 	mainWindowNeedsLayoutUpdate = false;
636 
637 	// properties must know what they stand for
638 	for (int32_t i = 0; i < C4ScriptGuiWindowPropertyName::_lastProp; ++i)
639 		props[i].type = i;
640 
641 	// standard values for all of the properties
642 
643 	// exact offsets are standard 0
644 	props[C4ScriptGuiWindowPropertyName::left].SetNull();
645 	props[C4ScriptGuiWindowPropertyName::right].SetNull();
646 	props[C4ScriptGuiWindowPropertyName::top].SetNull();
647 	props[C4ScriptGuiWindowPropertyName::bottom].SetNull();
648 	// relative offsets are standard full screen 0,0 - 1,1
649 	props[C4ScriptGuiWindowPropertyName::relLeft].SetNull();
650 	props[C4ScriptGuiWindowPropertyName::relTop].SetNull();
651 	props[C4ScriptGuiWindowPropertyName::relBottom].SetFloat(1.0f);
652 	props[C4ScriptGuiWindowPropertyName::relRight].SetFloat(1.0f);
653 	// all margins are always standard 0
654 	props[C4ScriptGuiWindowPropertyName::leftMargin].SetNull();
655 	props[C4ScriptGuiWindowPropertyName::rightMargin].SetNull();
656 	props[C4ScriptGuiWindowPropertyName::topMargin].SetNull();
657 	props[C4ScriptGuiWindowPropertyName::bottomMargin].SetNull();
658 	props[C4ScriptGuiWindowPropertyName::relLeftMargin].SetNull();
659 	props[C4ScriptGuiWindowPropertyName::relTopMargin].SetNull();
660 	props[C4ScriptGuiWindowPropertyName::relBottomMargin].SetNull();
661 	props[C4ScriptGuiWindowPropertyName::relRightMargin].SetNull();
662 	// other properties are 0
663 	props[C4ScriptGuiWindowPropertyName::backgroundColor].SetNull();
664 	props[C4ScriptGuiWindowPropertyName::frameDecoration].SetNull();
665 	props[C4ScriptGuiWindowPropertyName::symbolObject].SetNull();
666 	props[C4ScriptGuiWindowPropertyName::symbolDef].SetNull();
667 	props[C4ScriptGuiWindowPropertyName::text].SetNull();
668 	props[C4ScriptGuiWindowPropertyName::symbolGraphicsName].SetNull();
669 	props[C4ScriptGuiWindowPropertyName::tooltip].SetNull();
670 	props[C4ScriptGuiWindowPropertyName::onClickAction].SetNull();
671 	props[C4ScriptGuiWindowPropertyName::onMouseInAction].SetNull();
672 	props[C4ScriptGuiWindowPropertyName::onMouseOutAction].SetNull();
673 	props[C4ScriptGuiWindowPropertyName::onCloseAction].SetNull();
674 	props[C4ScriptGuiWindowPropertyName::style].SetNull();
675 	props[C4ScriptGuiWindowPropertyName::priority].SetNull();
676 	props[C4ScriptGuiWindowPropertyName::player].SetInt(ANY_OWNER);
677 
678 	wasRemoved = false;
679 	closeActionWasExecuted = false;
680 	currentMouseState = MouseState::None;
681 	target = nullptr;
682 	pScrollBar->fAutoHide = true;
683 
684 	rcBounds.x = rcBounds.y = 0;
685 	rcBounds.Wdt = rcBounds.Hgt = 0;
686 }
687 
~C4ScriptGuiWindow()688 C4ScriptGuiWindow::~C4ScriptGuiWindow()
689 {
690 	ClearChildren(false);
691 
692 	// delete certain properties that contain allocated elements or referenced strings
693 	for (auto & prop : props)
694 		prop.CleanUpAll();
695 
696 	if (pScrollBar)
697 		delete pScrollBar;
698 	if (name)
699 		name->DecRef();
700 }
701 
702 // helper function
SetMarginProperties(const C4Value & property,C4String * tag)703 void C4ScriptGuiWindow::SetMarginProperties(const C4Value &property, C4String *tag)
704 {
705 	// the value might be a tagged proplist again
706 	if (property.GetType() == C4V_Type::C4V_PropList)
707 	{
708 		C4PropList *proplist = property.getPropList();
709 		for (C4PropList::Iterator iter = proplist->begin(); iter != proplist->end(); ++iter)
710 		{
711 			SetMarginProperties(iter->Value, iter->Key);
712 		}
713 		return;
714 	}
715 
716 	// safety
717 	if (property.GetType() == C4V_Type::C4V_Array && property.getArray()->GetSize() == 0)
718 		return;
719 
720 	// always set all four margins
721 	for (int i = 0; i < 4; ++i)
722 	{
723 		C4ScriptGuiWindowPropertyName::type relative, absolute;
724 		switch (i)
725 		{
726 		case 0:
727 			absolute = C4ScriptGuiWindowPropertyName::leftMargin;
728 			relative = C4ScriptGuiWindowPropertyName::relLeftMargin;
729 			break;
730 		case 1:
731 			absolute = C4ScriptGuiWindowPropertyName::topMargin;
732 			relative = C4ScriptGuiWindowPropertyName::relTopMargin;
733 			break;
734 		case 2:
735 			absolute = C4ScriptGuiWindowPropertyName::rightMargin;
736 			relative = C4ScriptGuiWindowPropertyName::relRightMargin;
737 			break;
738 		case 3:
739 			absolute = C4ScriptGuiWindowPropertyName::bottomMargin;
740 			relative = C4ScriptGuiWindowPropertyName::relBottomMargin;
741 			break;
742 		default:
743 			assert(false);
744 		}
745 
746 		if (property.GetType() == C4V_Type::C4V_Array)
747 		{
748 			C4ValueArray *array = property.getArray();
749 			int realIndex = i % array->GetSize();
750 			SetPositionStringProperties(array->GetItem(realIndex), relative, absolute, tag);
751 		}
752 		else
753 			// normal string, hopefully
754 			SetPositionStringProperties(property, relative, absolute, tag);
755 	}
756 }
757 
MarginsToC4Value()758 C4Value C4ScriptGuiWindow::MarginsToC4Value()
759 {
760 	C4ValueArray *array = new C4ValueArray();
761 	array->SetSize(4);
762 
763 	array->SetItem(0, PositionToC4Value(C4ScriptGuiWindowPropertyName::relLeftMargin, C4ScriptGuiWindowPropertyName::leftMargin));
764 	array->SetItem(1, PositionToC4Value(C4ScriptGuiWindowPropertyName::relTopMargin, C4ScriptGuiWindowPropertyName::topMargin));
765 	array->SetItem(2, PositionToC4Value(C4ScriptGuiWindowPropertyName::relRightMargin, C4ScriptGuiWindowPropertyName::rightMargin));
766 	array->SetItem(3, PositionToC4Value(C4ScriptGuiWindowPropertyName::relBottomMargin, C4ScriptGuiWindowPropertyName::bottomMargin));
767 
768 	return C4Value(array);
769 }
770 
771 // helper function
SetPositionStringProperties(const C4Value & property,C4ScriptGuiWindowPropertyName::type relative,C4ScriptGuiWindowPropertyName::type absolute,C4String * tag)772 void C4ScriptGuiWindow::SetPositionStringProperties(const C4Value &property, C4ScriptGuiWindowPropertyName::type relative, C4ScriptGuiWindowPropertyName::type absolute, C4String *tag)
773 {
774 	// the value might be a tagged proplist again
775 	if (property.GetType() == C4V_Type::C4V_PropList)
776 	{
777 		C4PropList *proplist = property.getPropList();
778 		for (C4PropList::Iterator iter = proplist->begin(); iter != proplist->end(); ++iter)
779 		{
780 			SetPositionStringProperties(iter->Value, relative, absolute, iter->Key);
781 		}
782 		return;
783 	}
784 	// safety
785 	if (property.GetType() != C4V_Type::C4V_String) {
786 		if(property.GetType() != C4V_Type::C4V_Nil)
787 			LogF("Warning: Got %s instead of expected menu format string.", property.GetTypeName());
788 		return;
789 	}
790 
791 	float relativeValue = 0.0;
792 	float absoluteValue = 0.0;
793 
794 	std::locale c_locale("C");
795 	std::istringstream reader(std::string(property.getStr()->GetCStr()));
796 	reader.imbue(c_locale);
797 	if(!reader.good()) return;
798 
799 	while (!reader.eof())
800 	{
801 		reader >> std::ws; // eat white space
802 
803 		// look for next float
804 		float value;
805 		// here comes the fun.
806 		// strtod is locale dependent
807 		// istringstream will try to parse scientific notation, so things like 3em will be tried to be parsed as 3e<exponent> and consequently fail
808 		// thus, per stackoverflow recommendation, parse the float into a separate string and then let that be parsed
809 		std::stringstream floatss;
810 		floatss.imbue(c_locale);
811 		if(reader.peek() == '+' || reader.peek() == '-') floatss.put(reader.get());
812 		reader >> std::ws;
813 		while(std::isdigit(reader.peek()) || reader.peek() == '.') floatss.put(reader.get());
814 		floatss >> value;
815 		reader >> std::ws;
816 
817 		if (reader.peek() == '%')
818 		{
819 			relativeValue += value;
820 			reader.get();
821 		}
822 		else if (reader.get() == 'e' && reader.get() == 'm')
823 		{
824 			absoluteValue += value;
825 		}
826 		else // error, abort! (readere is not in a clean state anyway)
827 		{
828 			LogF(R"(Warning: Could not parse menu format string "%s"!)", property.getStr()->GetCStr());
829 			return;
830 		}
831 
832 		reader.peek(); // get eof bit to be set
833 	}
834 	props[relative].SetFloat(relativeValue / 100.0f, tag);
835 	props[absolute].SetFloat(absoluteValue, tag);
836 }
837 
838 // for saving
PositionToC4Value(C4ScriptGuiWindowPropertyName::type relativeName,C4ScriptGuiWindowPropertyName::type absoluteName)839 C4Value C4ScriptGuiWindow::PositionToC4Value(C4ScriptGuiWindowPropertyName::type relativeName, C4ScriptGuiWindowPropertyName::type absoluteName)
840 {
841 	// Go through all tags of the position attributes and save.
842 	// Note that the tags for both the relative and the absolute attribute are always the same.
843 	C4ScriptGuiWindowProperty &relative = props[relativeName];
844 	C4ScriptGuiWindowProperty &absolute = props[absoluteName];
845 
846 	C4PropList *proplist = nullptr;
847 	const bool onlyStdTag = relative.taggedProperties.size() == 1;
848 	for (auto & taggedProperty : relative.taggedProperties)
849 	{
850 		C4String *tag = taggedProperty.first;
851 		StdStrBuf buf;
852 		buf.Format("%f%%%+fem", 100.0f * taggedProperty.second.f, absolute.taggedProperties[tag].f);
853 		C4String *propString = Strings.RegString(buf);
854 
855 		if (onlyStdTag)
856 			return C4Value(propString);
857 		else
858 		{
859 			if (proplist == nullptr)
860 				proplist = C4PropList::New();
861 			proplist->SetPropertyByS(tag, C4Value(propString));
862 		}
863 	}
864 	return C4Value(proplist);
865 }
866 
Denumerate(C4ValueNumbers * numbers)867 void C4ScriptGuiWindow::Denumerate(C4ValueNumbers *numbers)
868 {
869 	assert(IsRoot());
870 	if (id == 0)
871 	{
872 		// nothing to do, note that the id is abused for the id in the enumeration
873 		return;
874 	}
875 	C4Value value =	numbers->GetValue(id);
876 	id = 0;
877 	CreateFromPropList(value.getPropList(), false, false, true);
878 
879 	for (C4GUI::Element * element : *this)
880 	{
881 		C4ScriptGuiWindow *mainWindow = static_cast<C4ScriptGuiWindow*>(element);
882 		mainWindow->RequestLayoutUpdate();
883 	}
884 }
885 
ToC4Value()886 const C4Value C4ScriptGuiWindow::ToC4Value()
887 {
888 	C4PropList *proplist = C4PropList::New();
889 
890 	// it is necessary that this list contains all of the properties which can also be set somehow
891 	// if you add something, don't forget to also add the real serialization to the loop below
892 	int32_t toSave[] =
893 	{
894 		P_Left,
895 		P_Top,
896 		P_Right,
897 		P_Bottom,
898 		P_Margin,
899 		P_BackgroundColor,
900 		P_Decoration,
901 		P_Symbol,
902 		P_Target,
903 		P_Text,
904 		P_ID,
905 		P_OnClick,
906 		P_OnMouseIn,
907 		P_OnMouseOut,
908 		P_OnClose,
909 		P_Style,
910 		P_Mode,
911 		P_Priority,
912 		P_Player,
913 		P_Tooltip
914 	};
915 
916 	const int32_t entryCount = sizeof(toSave) / sizeof(int32_t);
917 
918 	for (int prop : toSave)
919 	{
920 		C4Value val;
921 
922 		switch (prop)
923 		{
924 		case P_Left:
925 		case P_Top:
926 		case P_Right:
927 		case P_Bottom:
928 		{
929 #define PROPERTY_TUPLE(p, prop1, prop2) if (prop == p) { val = PositionToC4Value(prop1, prop2); }
930 			PROPERTY_TUPLE(P_Left, C4ScriptGuiWindowPropertyName::relLeft, C4ScriptGuiWindowPropertyName::left);
931 			PROPERTY_TUPLE(P_Top, C4ScriptGuiWindowPropertyName::relTop, C4ScriptGuiWindowPropertyName::top);
932 			PROPERTY_TUPLE(P_Right, C4ScriptGuiWindowPropertyName::relRight, C4ScriptGuiWindowPropertyName::right);
933 			PROPERTY_TUPLE(P_Bottom, C4ScriptGuiWindowPropertyName::relBottom, C4ScriptGuiWindowPropertyName::bottom);
934 #undef PROPERTY_TUPLE
935 			break;
936 		}
937 		case P_Margin: val = MarginsToC4Value(); break;
938 		case P_BackgroundColor: val = props[C4ScriptGuiWindowPropertyName::backgroundColor].ToC4Value(); break;
939 		case P_Decoration: val = props[C4ScriptGuiWindowPropertyName::frameDecoration].ToC4Value(); break;
940 		case P_Symbol:
941 			// either object or def
942 			val = props[C4ScriptGuiWindowPropertyName::symbolObject].ToC4Value();
943 			if (val == C4Value()) // is nil?
944 				val = props[C4ScriptGuiWindowPropertyName::symbolDef].ToC4Value();
945 			break;
946 		case P_Target: val = C4Value(target); break;
947 		case P_Text: val = props[C4ScriptGuiWindowPropertyName::text].ToC4Value(); break;
948 		case P_GraphicsName: val = props[C4ScriptGuiWindowPropertyName::symbolGraphicsName].ToC4Value(); break;
949 		case P_Tooltip: val = props[C4ScriptGuiWindowPropertyName::tooltip].ToC4Value(); break;
950 		case P_ID: val = C4Value(id); break;
951 		case P_OnClick: val = props[C4ScriptGuiWindowPropertyName::onClickAction].ToC4Value(); break;
952 		case P_OnMouseIn: val = props[C4ScriptGuiWindowPropertyName::onMouseInAction].ToC4Value(); break;
953 		case P_OnMouseOut: val = props[C4ScriptGuiWindowPropertyName::onMouseOutAction].ToC4Value(); break;
954 		case P_OnClose: val = props[C4ScriptGuiWindowPropertyName::onCloseAction].ToC4Value(); break;
955 		case P_Style: val = props[C4ScriptGuiWindowPropertyName::style].ToC4Value(); break;
956 		case P_Mode: val = C4Value(int32_t(currentMouseState)); break;
957 		case P_Priority: val = props[C4ScriptGuiWindowPropertyName::priority].ToC4Value(); break;
958 		case P_Player: val = props[C4ScriptGuiWindowPropertyName::player].ToC4Value(); break;
959 
960 		default:
961 			assert(false);
962 			break;
963 		}
964 
965 		// don't save "nil" values
966 		if (val == C4Value()) continue;
967 
968 		proplist->SetProperty(C4PropertyName(prop), val);
969 	}
970 
971 	// save children now, construct new names for them if necessary
972 	int32_t childIndex = 0;
973 	for (C4GUI::Element * element : *this)
974 	{
975 		C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
976 		C4Value val = child->ToC4Value();
977 		C4String *childName = child->name;
978 		if (!childName)
979 		{
980 			StdStrBuf childNameBuf;
981 			childNameBuf.Format("_child_%03d", ++childIndex);
982 			childName = Strings.RegString(childNameBuf);
983 		}
984 		proplist->SetPropertyByS(childName, val);
985 	}
986 
987 	return C4Value(proplist);
988 }
989 
CreateFromPropList(C4PropList * proplist,bool resetStdTag,bool isUpdate,bool isLoading)990 bool C4ScriptGuiWindow::CreateFromPropList(C4PropList *proplist, bool resetStdTag, bool isUpdate, bool isLoading)
991 {
992 	if (!proplist) return false;
993 	C4ScriptGuiWindow * parent = static_cast<C4ScriptGuiWindow*>(GetParent());
994 	assert((parent || isLoading) && "GuiWindow created from proplist without parent (fails for ID tag)");
995 
996 	bool layoutUpdateRequired = false; // needed for position changes etc
997 
998 	// Get properties from proplist and check for those, that match an allowed property to set them;
999 	// We take ownership here. Automatically destroy the object when we're done.
1000 	std::unique_ptr<C4ValueArray> properties(proplist->GetProperties());
1001 	properties->SortStrings();
1002 	C4String *stdTag = &Strings.P[P_Std];
1003 	const int32_t propertySize = properties->GetSize();
1004 	for (int32_t i = 0; i < propertySize; ++i)
1005 	{
1006 		const C4Value &entry = properties->GetItem(i);
1007 		C4String *key = entry.getStr();
1008 		assert(key && "PropList returns non-string as key");
1009 		MenuDebugLogF("--%s\t\t(I am %d)", key->GetCStr(), id);
1010 		C4Value property;
1011 		proplist->GetPropertyByS(key, &property);
1012 
1013 		if(&Strings.P[P_Left] == key)
1014 		{
1015 			SetPositionStringProperties(property, C4ScriptGuiWindowPropertyName::relLeft, C4ScriptGuiWindowPropertyName::left, stdTag);
1016 			layoutUpdateRequired = true;
1017 		}
1018 		else if(&Strings.P[P_Top] == key)
1019 		{
1020 			SetPositionStringProperties(property, C4ScriptGuiWindowPropertyName::relTop, C4ScriptGuiWindowPropertyName::top, stdTag);
1021 			layoutUpdateRequired = true;
1022 		}
1023 		else if(&Strings.P[P_Right] == key)
1024 		{
1025 			SetPositionStringProperties(property, C4ScriptGuiWindowPropertyName::relRight, C4ScriptGuiWindowPropertyName::right, stdTag);
1026 			layoutUpdateRequired = true;
1027 		}
1028 		else if(&Strings.P[P_Bottom] == key)
1029 		{
1030 			SetPositionStringProperties(property, C4ScriptGuiWindowPropertyName::relBottom, C4ScriptGuiWindowPropertyName::bottom, stdTag);
1031 			layoutUpdateRequired = true;
1032 		}
1033 		else if (&Strings.P[P_Margin] == key)
1034 		{
1035 			SetMarginProperties(property, stdTag);
1036 			layoutUpdateRequired = true;
1037 		}
1038 		else if(&Strings.P[P_BackgroundColor] == key)
1039 			props[C4ScriptGuiWindowPropertyName::backgroundColor].Set(property, stdTag);
1040 		else if(&Strings.P[P_Target] == key)
1041 			target = property.getObj();
1042 		else if(&Strings.P[P_Symbol] == key)
1043 		{
1044 			props[C4ScriptGuiWindowPropertyName::symbolDef].Set(property, stdTag);
1045 			props[C4ScriptGuiWindowPropertyName::symbolObject].Set(property, stdTag);
1046 		}
1047 		else if(&Strings.P[P_Decoration] == key)
1048 		{
1049 			props[C4ScriptGuiWindowPropertyName::frameDecoration].Set(property, stdTag);
1050 		}
1051 		else if(&Strings.P[P_Text] == key)
1052 		{
1053 			props[C4ScriptGuiWindowPropertyName::text].Set(property, stdTag);
1054 			layoutUpdateRequired = true;
1055 		}
1056 		else if (&Strings.P[P_GraphicsName] == key)
1057 		{
1058 			props[C4ScriptGuiWindowPropertyName::symbolGraphicsName].Set(property, stdTag);
1059 		}
1060 		else if (&Strings.P[P_Tooltip] == key)
1061 		{
1062 			props[C4ScriptGuiWindowPropertyName::tooltip].Set(property, stdTag);
1063 		}
1064 		else if(&Strings.P[P_Prototype] == key)
1065 			; // do nothing
1066 		else if (&Strings.P[P_Mode] == key) // note that "Mode" is abused here for saving whether we have mouse focus
1067 		{
1068 			if (isLoading)
1069 				currentMouseState = property.getInt();
1070 		}
1071 		else if(&Strings.P[P_ID] == key)
1072 		{
1073 			// setting IDs is only valid for subwindows or when loading savegames!
1074 			if (parent && !isMainWindow)
1075 			{
1076 				if (id) // already have an ID? remove from parent
1077 					parent->ChildWithIDRemoved(this);
1078 				id = property.getInt();
1079 				if (id != 0)
1080 					parent->ChildGotID(this);
1081 			}
1082 			else
1083 				if (isLoading)
1084 					id = property.getInt();
1085 		}
1086 		else if (&Strings.P[P_OnClick] == key)
1087 		{
1088 			MenuDebugLogF("Adding new action, I am window %d with parent %d", id, static_cast<C4ScriptGuiWindow*>(parent)->id);
1089 			props[C4ScriptGuiWindowPropertyName::onClickAction].Set(property, stdTag);
1090 		}
1091 		else if(&Strings.P[P_OnMouseIn] == key)
1092 			props[C4ScriptGuiWindowPropertyName::onMouseInAction].Set(property, stdTag);
1093 		else if(&Strings.P[P_OnMouseOut] == key)
1094 			props[C4ScriptGuiWindowPropertyName::onMouseOutAction].Set(property, stdTag);
1095 		else if(&Strings.P[P_OnClose] == key)
1096 			props[C4ScriptGuiWindowPropertyName::onCloseAction].Set(property, stdTag);
1097 		else if(&Strings.P[P_Style] == key)
1098 		{
1099 			props[C4ScriptGuiWindowPropertyName::style].Set(property, stdTag);
1100 			layoutUpdateRequired = true;
1101 		}
1102 		else if(&Strings.P[P_Priority] == key)
1103 		{
1104 			props[C4ScriptGuiWindowPropertyName::priority].Set(property, stdTag);
1105 			layoutUpdateRequired = true;
1106 			// resort into parent's list
1107 			if (parent)
1108 				parent->ChildChangedPriority(this);
1109 		}
1110 		else if(&Strings.P[P_Player] == key)
1111 			props[C4ScriptGuiWindowPropertyName::player].Set(property, stdTag);
1112 		else
1113 		{
1114 			// possibly sub-window?
1115 			C4PropList *subwindow = property.getPropList();
1116 			if (subwindow)
1117 			{
1118 				// remember the name of the child; but ignore names starting with underscores
1119 				C4String *childName = nullptr;
1120 				if (key->GetCStr()[0] != '_')
1121 					childName = key;
1122 
1123 				// Do we already have a child with that name? That implies that we are updating here.
1124 				C4ScriptGuiWindow *child = GetChildByName(childName);
1125 				bool freshlyAdded = false;
1126 
1127 				// first time referencing a child with that name? Create a new one!
1128 				if (!child)
1129 				{
1130 					child = new C4ScriptGuiWindow();
1131 					if (childName != nullptr)
1132 					{
1133 						child->name = childName;
1134 						child->name->IncRef();
1135 					}
1136 					AddChild(child);
1137 					freshlyAdded = true;
1138 				}
1139 
1140 				if (!child->CreateFromPropList(subwindow, isUpdate == true, false, isLoading))
1141 				{
1142 					// Remove the child again if we just added it. However, ignore when just updating an existing child.
1143 					if (freshlyAdded)
1144 						RemoveChild(child, false);
1145 				}
1146 				else
1147 					layoutUpdateRequired = true;
1148 			}
1149 		}
1150 	}
1151 
1152 	if (!isLoading && layoutUpdateRequired)
1153 		RequestLayoutUpdate();
1154 
1155 	if (resetStdTag || isLoading)
1156 		SetTag(stdTag);
1157 
1158 	return true;
1159 }
1160 
ClearPointers(C4Object * pObj)1161 void C4ScriptGuiWindow::ClearPointers(C4Object *pObj)
1162 {
1163 	// not removing or clearing anything twice
1164 	// if this flag is set, the object will not be used after this frame (callbacks?) anyway
1165 	if (wasRemoved) return;
1166 
1167 	if (target == pObj)
1168 	{
1169 		Close();
1170 		return;
1171 	}
1172 
1173 	// all properties which have anything to do with objects need to be called from here!
1174 	props[C4ScriptGuiWindowPropertyName::symbolObject].ClearPointers(pObj);
1175 	props[C4ScriptGuiWindowPropertyName::onClickAction].ClearPointers(pObj);
1176 	props[C4ScriptGuiWindowPropertyName::onMouseInAction].ClearPointers(pObj);
1177 	props[C4ScriptGuiWindowPropertyName::onMouseOutAction].ClearPointers(pObj);
1178 	props[C4ScriptGuiWindowPropertyName::onCloseAction].ClearPointers(pObj);
1179 
1180 	for (auto iter = begin(); iter != end();)
1181 	{
1182 		C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(*iter);
1183 		// increment the iterator before (possibly) deleting the child
1184 		++iter;
1185 		child->ClearPointers(pObj);
1186 	}
1187 }
1188 
AddChild(C4ScriptGuiWindow * child)1189 C4ScriptGuiWindow *C4ScriptGuiWindow::AddChild(C4ScriptGuiWindow *child)
1190 {
1191 	if (IsRoot())
1192 	{
1193 		child->SetID(GenerateMenuID());
1194 		child->isMainWindow = true;
1195 		// update all windows asap
1196 		mainWindowNeedsLayoutUpdate = true;
1197 	}
1198 
1199 	// child's priority is ususally 0 here, so just insert it in front of other windows with a priority below 0
1200 	// when the child's priority updates, the update function will be called and the child will be sorted to the correct position
1201 	ChildChangedPriority(child);
1202 
1203 	return child;
1204 }
1205 
ChildChangedPriority(C4ScriptGuiWindow * child)1206 void C4ScriptGuiWindow::ChildChangedPriority(C4ScriptGuiWindow *child)
1207 {
1208 	int prio = child->props[C4ScriptGuiWindowPropertyName::priority].GetInt();
1209 	C4GUI::Element * insertBefore = nullptr;
1210 
1211 	for (C4GUI::Element * element : *this)
1212 	{
1213 		C4ScriptGuiWindow * otherChild = static_cast<C4ScriptGuiWindow*>(element);
1214 		if (otherChild->props[C4ScriptGuiWindowPropertyName::priority].GetInt() <= prio) continue;
1215 		insertBefore = element;
1216 		break;
1217 	}
1218 	// if the child is already at the correct position, do nothing
1219 	assert(child != insertBefore);
1220 	// resort
1221 	// this method will take care of removing and re-adding the child
1222 	InsertElement(child, insertBefore);
1223 }
1224 
ChildWithIDRemoved(C4ScriptGuiWindow * child)1225 void C4ScriptGuiWindow::ChildWithIDRemoved(C4ScriptGuiWindow *child)
1226 {
1227 	if (IsRoot()) return;
1228 	if (!isMainWindow)
1229 		return static_cast<C4ScriptGuiWindow*>(GetParent())->ChildWithIDRemoved(child);
1230 	std::pair<std::multimap<int32_t, C4ScriptGuiWindow*>::iterator, std::multimap<int32_t, C4ScriptGuiWindow*>::iterator> range;
1231 	range = childrenIDMap.equal_range(child->GetID());
1232 
1233 	for (std::multimap<int32_t, C4ScriptGuiWindow*>::iterator iter = range.first; iter != range.second; ++iter)
1234 	{
1235 		if (iter->second != child) continue;
1236 		childrenIDMap.erase(iter);
1237 		MenuDebugLogF("child-map-size: %d, remove %d [I am %d]", childrenIDMap.size(), child->GetID(), id);
1238 		return;
1239 	}
1240 }
1241 
ChildGotID(C4ScriptGuiWindow * child)1242 void C4ScriptGuiWindow::ChildGotID(C4ScriptGuiWindow *child)
1243 {
1244 	assert(!IsRoot() && "ChildGotID called on window root, should not propagate over main windows!");
1245 	if (!isMainWindow)
1246 		return static_cast<C4ScriptGuiWindow*>(GetParent())->ChildGotID(child);
1247 	childrenIDMap.insert(std::make_pair(child->GetID(), child));
1248 	MenuDebugLogF("child+map+size: %d, added %d [I am %d]", childrenIDMap.size(), child->GetID(), id);
1249 }
1250 
GetChildByID(int32_t childID)1251 C4ScriptGuiWindow *C4ScriptGuiWindow::GetChildByID(int32_t childID)
1252 {
1253 	for (Element * element : *this)
1254 	{
1255 		C4ScriptGuiWindow * child = static_cast<C4ScriptGuiWindow*>(element);
1256 		if (child->id != childID) continue;
1257 		return child;
1258 	}
1259 	return nullptr;
1260 }
1261 
GetChildByName(C4String * childName)1262 C4ScriptGuiWindow *C4ScriptGuiWindow::GetChildByName(C4String *childName)
1263 {
1264 	// invalid child names never match
1265 	if (childName == nullptr) return nullptr;
1266 
1267 	for (Element * element : *this)
1268 	{
1269 		C4ScriptGuiWindow * child = static_cast<C4ScriptGuiWindow*>(element);
1270 		// every C4String is unique, so we can compare pointers here
1271 		if (child->name != childName) continue;
1272 		return child;
1273 	}
1274 	return nullptr;
1275 }
1276 
GetSubWindow(int32_t childID,C4Object * childTarget)1277 C4ScriptGuiWindow *C4ScriptGuiWindow::GetSubWindow(int32_t childID, C4Object *childTarget)
1278 {
1279 	std::pair<std::multimap<int32_t, C4ScriptGuiWindow*>::iterator, std::multimap<int32_t, C4ScriptGuiWindow*>::iterator> range;
1280 	range = childrenIDMap.equal_range(childID);
1281 
1282 	for (std::multimap<int32_t, C4ScriptGuiWindow*>::iterator iter = range.first; iter != range.second; ++iter)
1283 	{
1284 		C4ScriptGuiWindow *subwindow = iter->second;
1285 		if (subwindow->GetTarget() != childTarget) continue;
1286 		return subwindow;
1287 	}
1288 	return nullptr;
1289 }
1290 
RemoveChild(C4ScriptGuiWindow * child,bool close,bool all)1291 void C4ScriptGuiWindow::RemoveChild(C4ScriptGuiWindow *child, bool close, bool all)
1292 {
1293 	// do a layout update asap
1294 	if (!all && !IsRoot())
1295 		RequestLayoutUpdate();
1296 
1297 	if (child)
1298 	{
1299 		child->wasRemoved = true;
1300 		if (close) child->Close();
1301 		if (child->GetID() != 0)
1302 			ChildWithIDRemoved(child);
1303 		RemoveElement(static_cast<C4GUI::Element*>(child));
1304 		// RemoveElement does NOT delete the child itself.
1305 		delete child;
1306 	}
1307 	else if (close) // close all children
1308 	{
1309 		assert(all);
1310 		for (Element * element : *this)
1311 		{
1312 			C4ScriptGuiWindow * child = static_cast<C4ScriptGuiWindow*>(element);
1313 			child->wasRemoved = true;
1314 			child->Close();
1315 			if (child->GetID() != 0)
1316 				ChildWithIDRemoved(child);
1317 		}
1318 	}
1319 
1320 	if (all)
1321 		C4GUI::ScrollWindow::ClearChildren();
1322 }
1323 
ClearChildren(bool close)1324 void C4ScriptGuiWindow::ClearChildren(bool close)
1325 {
1326 	RemoveChild(nullptr, close, true);
1327 }
1328 
Close()1329 void C4ScriptGuiWindow::Close()
1330 {
1331 	// first, close all children and dispose of them properly
1332 	ClearChildren(true);
1333 
1334 	if (!closeActionWasExecuted)
1335 	{
1336 		closeActionWasExecuted = true;
1337 
1338 		// make call to target object if applicable
1339 		C4ScriptGuiWindowAction *action = props[C4ScriptGuiWindowPropertyName::onCloseAction].GetAction();
1340 		// only calls are valid actions for OnClose
1341 		if (action && action->action == C4ScriptGuiWindowActionID::Call)
1342 		{
1343 			// close is always syncronized (script call/object removal) and thus the action can be executed immediately
1344 			// (otherwise the GUI&action would have been removed anyway..)
1345 			action->ExecuteCommand(action->id, this, NO_OWNER);
1346 		}
1347 	}
1348 
1349 	if (!wasRemoved)
1350 	{
1351 		assert(GetParent() && "Close()ing GUIWindow without parent");
1352 		static_cast<C4ScriptGuiWindow*>(GetParent())->RemoveChild(this);
1353 	}
1354 }
1355 
EnableScrollBar(bool enable,float childrenHeight)1356 void C4ScriptGuiWindow::EnableScrollBar(bool enable, float childrenHeight)
1357 {
1358 	const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
1359 
1360 	if (style & C4ScriptGuiWindowStyleFlag::FitChildren)
1361 	{
1362 		float height = float(rcBounds.Hgt)
1363 				- Em2Pix(props[C4ScriptGuiWindowPropertyName::topMargin].GetFloat())
1364 				- Em2Pix(props[C4ScriptGuiWindowPropertyName::bottomMargin].GetFloat());
1365 		float adjustment = childrenHeight - height;
1366 		props[C4ScriptGuiWindowPropertyName::bottom].current->f += Pix2Em(adjustment);
1367 		assert(!std::isnan(props[C4ScriptGuiWindowPropertyName::bottom].current->f));
1368 		// instantly pseudo-update the sizes in case of multiple refreshs before the next draw
1369 		rcBounds.Hgt += adjustment;
1370 		// parents that are somehow affected by their children will need to refresh their layout
1371 		if (adjustment != 0.0)
1372 			RequestLayoutUpdate();
1373 		return;
1374 	}
1375 
1376 	if (style & C4ScriptGuiWindowStyleFlag::NoCrop) return;
1377 
1378 	C4GUI::ScrollWindow::SetScrollBarEnabled(enable, true);
1379 }
1380 
1381 
CalculateRelativeSize(float parentWidthOrHeight,C4ScriptGuiWindowPropertyName::type absoluteProperty,C4ScriptGuiWindowPropertyName::type relativeProperty)1382 float C4ScriptGuiWindow::CalculateRelativeSize(float parentWidthOrHeight, C4ScriptGuiWindowPropertyName::type absoluteProperty, C4ScriptGuiWindowPropertyName::type relativeProperty)
1383 {
1384 	const float widthOrHeight = Em2Pix(props[absoluteProperty].GetFloat())
1385 		+ float(parentWidthOrHeight) * props[relativeProperty].GetFloat();
1386 	return widthOrHeight;
1387 }
1388 
1389 
UpdateLayoutGrid()1390 void C4ScriptGuiWindow::UpdateLayoutGrid()
1391 {
1392 	const int32_t &width = rcBounds.Wdt;
1393 	const int32_t &height = rcBounds.Hgt;
1394 
1395 	const int32_t borderX(0), borderY(0);
1396 	int32_t currentX = borderX;
1397 	int32_t currentY = borderY;
1398 	int32_t lowestChildRelY = 0;
1399 	int32_t maxChildHeight = 0;
1400 
1401 	for (C4GUI::Element * element : *this)
1402 	{
1403 		C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1404 		// calculate the space the child needs, correctly respecting the margins
1405 		using namespace C4ScriptGuiWindowPropertyName;
1406 		const float childLeftMargin = child->CalculateRelativeSize(width, leftMargin, relLeftMargin);
1407 		const float childTopMargin = child->CalculateRelativeSize(height, topMargin, relTopMargin);
1408 		const float childRightMargin = child->CalculateRelativeSize(width, rightMargin, relRightMargin);
1409 		const float childBottomMargin = child->CalculateRelativeSize(height, bottomMargin, relBottomMargin);
1410 
1411 		const float childWdtF = float(child->rcBounds.Wdt) + childLeftMargin + childRightMargin;
1412 		const float childHgtF = float(child->rcBounds.Hgt) + childTopMargin + childBottomMargin;
1413 
1414 		auto doLineBreak = [&]()
1415 		{
1416 			currentX = borderX;
1417 			currentY += maxChildHeight + borderY;
1418 			maxChildHeight = 0;
1419 		};
1420 
1421 		// do all the rounding after the calculations
1422 		const auto childWdt = (int32_t)(childWdtF + 0.5f);
1423 		const auto childHgt = (int32_t)(childHgtF + 0.5f);
1424 
1425 		// Check if the child even fits in the remainder of the row
1426 		const bool fitsInRow = (width - currentX) >= childWdt;
1427 		if (!fitsInRow) doLineBreak();
1428 
1429 		// remember the highest child to make sure rows don't overlap
1430 		if (!maxChildHeight || (childHgt > maxChildHeight))
1431 		{
1432 			maxChildHeight = childHgt;
1433 			lowestChildRelY = currentY + childHgt;
1434 		}
1435 		child->rcBounds.x = currentX + static_cast<int32_t>(childLeftMargin);
1436 		child->rcBounds.y = currentY + static_cast<int32_t>(childTopMargin);
1437 
1438 		currentX += childWdt + borderX;
1439 	}
1440 
1441 	// do we need a scroll bar?
1442 	EnableScrollBar(lowestChildRelY > height, lowestChildRelY);
1443 }
1444 
1445 // Similar to the grid layout but tries to fill spaces more thoroughly.
1446 // It's slower and might reorder items.
UpdateLayoutTightGrid()1447 void C4ScriptGuiWindow::UpdateLayoutTightGrid()
1448 {
1449 	const int32_t &width = rcBounds.Wdt;
1450 	const int32_t &height = rcBounds.Hgt;
1451 	const int32_t borderX(0), borderY(0);
1452 	int32_t lowestChildRelY = 0;
1453 
1454 	std::list<C4ScriptGuiWindow*> alreadyPlacedChildren;
1455 
1456 	for (C4GUI::Element * element : *this)
1457 	{
1458 		C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1459 		// calculate the space the child needs, correctly respecting the margins
1460 		using namespace C4ScriptGuiWindowPropertyName;
1461 		const float childLeftMargin = child->CalculateRelativeSize(width, leftMargin, relLeftMargin);
1462 		const float childTopMargin = child->CalculateRelativeSize(height, topMargin, relTopMargin);
1463 		const float childRightMargin = child->CalculateRelativeSize(width, rightMargin, relRightMargin);
1464 		const float childBottomMargin = child->CalculateRelativeSize(height, bottomMargin, relBottomMargin);
1465 
1466 		const float childWdtF = float(child->rcBounds.Wdt) + childLeftMargin + childRightMargin;
1467 		const float childHgtF = float(child->rcBounds.Hgt) + childTopMargin + childBottomMargin;
1468 
1469 		// do all the rounding after the calculations
1470 		const auto childWdt = (int32_t)(childWdtF + 0.5f);
1471 		const auto childHgt = (int32_t)(childHgtF + 0.5f);
1472 
1473 		// Look for a free spot.
1474 		int32_t currentX = borderX;
1475 		int32_t currentY = borderY;
1476 
1477 		bool hadOverlap = false;
1478 		int overlapRepeats = 0;
1479 		do
1480 		{
1481 			auto overlapsWithOther = [&currentX, &currentY, &childWdt, &childHgt](C4ScriptGuiWindow *other)
1482 			{
1483 				if (currentX + childWdt <= other->rcBounds.x) return false;
1484 				if (currentY + childHgt <= other->rcBounds.y) return false;
1485 				if (currentX >= other->rcBounds.GetRight()) return false;
1486 				if (currentY >= other->rcBounds.GetBottom()) return false;
1487 				return true;
1488 			};
1489 
1490 			int32_t currentMinY = 0;
1491 			hadOverlap = false;
1492 			for (auto &other : alreadyPlacedChildren)
1493 			{
1494 				// Check if the other element is not yet above the new child.
1495 				if ((other->rcBounds.GetBottom() > currentY) && other->rcBounds.Hgt > 0)
1496 				{
1497 					if (currentMinY == 0 || (other->rcBounds.GetBottom() < currentMinY))
1498 						currentMinY = other->rcBounds.GetBottom();
1499 				}
1500 				// If overlapping, we must advance.
1501 				if (overlapsWithOther(other))
1502 				{
1503 					hadOverlap = true;
1504 					currentX = other->rcBounds.GetRight();
1505 					// Break line if the element doesn't fit anymore.
1506 					if (currentX + childWdt > width)
1507 					{
1508 						currentX = borderX;
1509 						// Start forcing change once we start repeating the check. Otherwise, there might
1510 						// be a composition of children that lead to infinite loop. The worst-case number
1511 						// of sensible checks might O(N^2) be with a really unfortunate children list.
1512 						const int32_t forcedMinimalChange = (overlapRepeats > alreadyPlacedChildren.size()) ? 1 : 0;
1513 						currentY = std::max(currentY + forcedMinimalChange, currentMinY);
1514 					}
1515 				}
1516 			}
1517 			overlapRepeats += 1;
1518 		} while (hadOverlap);
1519 
1520 		alreadyPlacedChildren.push_back(child);
1521 
1522 		lowestChildRelY = std::max(lowestChildRelY, currentY + childHgt);
1523 		child->rcBounds.x = currentX + static_cast<int32_t>(childLeftMargin);
1524 		child->rcBounds.y = currentY + static_cast<int32_t>(childTopMargin);
1525 	}
1526 
1527 	// do we need a scroll bar?
1528 	EnableScrollBar(lowestChildRelY > height, lowestChildRelY);
1529 }
1530 
UpdateLayoutVertical()1531 void C4ScriptGuiWindow::UpdateLayoutVertical()
1532 {
1533 	const int32_t borderY(0);
1534 	int32_t currentY = borderY;
1535 
1536 	for (C4GUI::Element * element : *this)
1537 	{
1538 		C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1539 
1540 		// Do the calculations in floats first to not lose accuracy.
1541 		// Take the height of the child and then add the margins.
1542 		using namespace C4ScriptGuiWindowPropertyName;
1543 		const float childTopMargin = child->CalculateRelativeSize(rcBounds.Hgt, topMargin, relTopMargin);
1544 		const float childBottomMargin = child->CalculateRelativeSize(rcBounds.Hgt, bottomMargin, relBottomMargin);
1545 
1546 		const float childHgtF = float(child->rcBounds.Hgt) + childTopMargin + childBottomMargin;
1547 		const int32_t childHgt = (int32_t)(childHgtF + 0.5f);
1548 
1549 		child->rcBounds.y = currentY + childTopMargin;
1550 		currentY += childHgt + borderY;
1551 	}
1552 
1553 	// do we need a scroll bar?
1554 	EnableScrollBar(currentY > rcBounds.Hgt, currentY);
1555 }
1556 
DrawChildren(C4TargetFacet & cgo,int32_t player,int32_t withMultipleFlag,C4Rect * currentClippingRect)1557 bool C4ScriptGuiWindow::DrawChildren(C4TargetFacet &cgo, int32_t player, int32_t withMultipleFlag, C4Rect *currentClippingRect)
1558 {
1559 	// remember old target rectangle and adjust
1560 	float oldTargetX = cgo.TargetX;
1561 	float oldTargetY = cgo.TargetY;
1562 	C4Rect myClippingRect;
1563 	if (IsRoot())
1564 	{
1565 		cgo.TargetX = 0;
1566 		cgo.TargetY = 0;
1567 		pDraw->StorePrimaryClipper();
1568 		// default: full screen clipper
1569 		myClippingRect = C4Rect(0, 0, cgo.Wdt * cgo.Zoom, cgo.Hgt * cgo.Zoom);
1570 		currentClippingRect = &myClippingRect;
1571 	}
1572 
1573 	// if ANY PARENT has scroll bar, then adjust clipper
1574 	int32_t clipX1(0), clipX2(0), clipY1(0), clipY2(0);
1575 	bool clipping = GetClippingRect(clipX1, clipY1, clipX2, clipY2);
1576 
1577 	const int32_t targetClipX1 = cgo.X + cgo.TargetX + clipX1;
1578 	const int32_t targetClipY1 = cgo.Y + cgo.TargetY + clipY1;
1579 	const int32_t targetClipX2 = cgo.X + cgo.TargetX + clipX2;
1580 	const int32_t targetClipY2 = cgo.Y + cgo.TargetY + clipY2;
1581 
1582 	if (clipping)
1583 	{
1584 		// Take either the parent rectangle or restrict it additionally by the child's geometry.
1585 		myClippingRect = C4Rect(
1586 			std::max(currentClippingRect->x, targetClipX1),
1587 			std::max(currentClippingRect->y, targetClipY1),
1588 			std::min(currentClippingRect->Wdt, targetClipX2),
1589 			std::min(currentClippingRect->Hgt, targetClipY2));
1590 		currentClippingRect = &myClippingRect;
1591 	}
1592 
1593 	if (withMultipleFlag != 1)
1594 	{
1595 		cgo.TargetX += rcBounds.x;
1596 		cgo.TargetY += rcBounds.y - iScrollY;
1597 	}
1598 	else
1599 	{
1600 		assert(IsRoot());
1601 		assert(withMultipleFlag == 1);
1602 	}
1603 
1604 
1605 	// note that withMultipleFlag only plays a roll for the root-menu
1606 	bool oneDrawn = false; // was at least one child drawn?
1607 	//for (auto iter = rbegin(); iter != rend(); ++iter)
1608 	for (auto element : *this)
1609 	{
1610 		C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1611 
1612 		if (withMultipleFlag != -1)
1613 		{
1614 			const int32_t &style = child->props[C4ScriptGuiWindowPropertyName::style].GetInt();
1615 			if ((withMultipleFlag == 0) && (style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
1616 			if ((withMultipleFlag == 1) && !(style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
1617 		}
1618 
1619 		pDraw->SetPrimaryClipper(currentClippingRect->x, currentClippingRect->y, currentClippingRect->Wdt, currentClippingRect->Hgt);
1620 
1621 		if (child->Draw(cgo, player, currentClippingRect))
1622 			oneDrawn = true;
1623 		// draw only one window when drawing non-Multiple windows
1624 		if (oneDrawn && (withMultipleFlag == 0)) break;
1625 	}
1626 
1627 	// Scrolling obviously does not affect the scroll bar.
1628 	cgo.TargetY += iScrollY;
1629 	// The scroll bar does not correct for the cgo offset (i.e. the upper board).
1630 	cgo.TargetX += cgo.X;
1631 	cgo.TargetY += cgo.Y;
1632 
1633 	if (pScrollBar->IsVisible())
1634 		pScrollBar->DrawElement(cgo);
1635 
1636 	if (IsRoot())
1637 	{
1638 		pDraw->RestorePrimaryClipper();
1639 	}
1640 
1641 	// restore target rectangle
1642 	cgo.TargetX = oldTargetX;
1643 	cgo.TargetY = oldTargetY;
1644 	return oneDrawn;
1645 }
1646 
RequestLayoutUpdate()1647 void C4ScriptGuiWindow::RequestLayoutUpdate()
1648 {
1649 	// directly requested on the root window?
1650 	// That usually comes from another part of the engine (f.e. C4Viewport::RecalculateViewports) or from a multiple-window child
1651 	if (!GetParent())
1652 	{
1653 		mainWindowNeedsLayoutUpdate = true;
1654 		return;
1655 	}
1656 
1657 	// are we a direct child of the root?
1658 	if (isMainWindow)
1659 	{
1660 		const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
1661 
1662 		if (!(style & C4ScriptGuiWindowStyleFlag::Multiple)) // are we a simple centered window?
1663 		{
1664 			mainWindowNeedsLayoutUpdate = true;
1665 			return;
1666 		}
1667 		else // we are one of the multiple windows.. the root better do a full refresh
1668 		{}
1669 	}
1670 	// propagate to parent window
1671 	static_cast<C4ScriptGuiWindow*>(GetParent())->RequestLayoutUpdate();
1672 }
1673 
UpdateChildLayout(C4TargetFacet & cgo,float parentWidth,float parentHeight)1674 bool C4ScriptGuiWindow::UpdateChildLayout(C4TargetFacet &cgo, float parentWidth, float parentHeight)
1675 {
1676 	for (Element * element : *this)
1677 	{
1678 		C4ScriptGuiWindow *window = static_cast<C4ScriptGuiWindow*>(element);
1679 		window->UpdateLayout(cgo, parentWidth, parentHeight);
1680 	}
1681 	return true;
1682 }
1683 
UpdateLayout(C4TargetFacet & cgo)1684 bool C4ScriptGuiWindow::UpdateLayout(C4TargetFacet &cgo)
1685 {
1686 	assert(IsRoot()); // we are root
1687 
1688 	// assume I am the root and use the a special rectangle in the viewport for drawing
1689 	const float fullWidth = cgo.Wdt * cgo.Zoom - cgo.X;
1690 	const float fullHeight = cgo.Hgt * cgo.Zoom - cgo.Y;
1691 
1692 	// golden ratio defined default size!
1693 	const float &targetWidthEm = C4ScriptGuiWindow::standardWidth;
1694 	const float &targetHeightEm = C4ScriptGuiWindow::standardHeight;
1695 
1696 	// adjust by viewport size
1697 	const float minMarginPx = 50.0f;
1698 	const float targetWidthPx = std::min(Em2Pix(targetWidthEm), fullWidth - 2.0f * minMarginPx);
1699 	const float targetHeightPx = std::min(Em2Pix(targetHeightEm), fullHeight - 2.0f * minMarginPx);
1700 
1701 	// calculate margins to center the window
1702 	const float marginLeftRight = (fullWidth - targetWidthPx) / 2.0f;
1703 	const float marginTopBottom = (fullHeight- targetHeightPx) / 2.0f;
1704 
1705 	// we can only position the window by adjusting left/right/top/bottom
1706 	const float &left = marginLeftRight;
1707 	const float right = -marginLeftRight;
1708 	const float &top = marginTopBottom;
1709 	const float bottom = -marginTopBottom;
1710 
1711 	// actual size, calculated from borders
1712 	const float wdt = fullWidth - left + right;
1713 	const float hgt = fullHeight - top + bottom;
1714 
1715 	const bool needUpdate = mainWindowNeedsLayoutUpdate || (rcBounds.Wdt != int32_t(wdt)) || (rcBounds.Hgt != int32_t(hgt));
1716 
1717 	if (needUpdate)
1718 	{
1719 		mainWindowNeedsLayoutUpdate = false;
1720 
1721 		// these are the coordinates for the centered non-multiple windows
1722 		rcBounds.x = static_cast<int>(left);
1723 		rcBounds.y = static_cast<int>(top);
1724 		rcBounds.Wdt = wdt;
1725 		rcBounds.Hgt = hgt;
1726 
1727 		// first update all multiple windows (that can cover the whole screen)
1728 		for (Element * element : *this)
1729 		{
1730 			C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1731 			const int32_t &style = child->props[C4ScriptGuiWindowPropertyName::style].GetInt();
1732 			if (!(style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
1733 			child->UpdateLayout(cgo, fullWidth, fullHeight);
1734 		}
1735 		// then update all "main" windows in the center of the screen
1736 		// todo: adjust the size of the main window based on the border-windows drawn before
1737 		for (Element * element : *this)
1738 		{
1739 			C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1740 			const int32_t &style = child->props[C4ScriptGuiWindowPropertyName::style].GetInt();
1741 			if ((style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
1742 			child->UpdateLayout(cgo, wdt, hgt);
1743 		}
1744 
1745 		pScrollBar->SetVisibility(false);
1746 	}
1747 	return true;
1748 }
1749 
UpdateLayout(C4TargetFacet & cgo,float parentWidth,float parentHeight)1750 bool C4ScriptGuiWindow::UpdateLayout(C4TargetFacet &cgo, float parentWidth, float parentHeight)
1751 {
1752 	// fetch style
1753 	const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
1754 	// fetch current position as shortcut for overview
1755 	const float &left = props[C4ScriptGuiWindowPropertyName::left].GetFloat();
1756 	const float &right = props[C4ScriptGuiWindowPropertyName::right].GetFloat();
1757 	const float &top = props[C4ScriptGuiWindowPropertyName::top].GetFloat();
1758 	const float &bottom = props[C4ScriptGuiWindowPropertyName::bottom].GetFloat();
1759 
1760 	const float &relLeft = props[C4ScriptGuiWindowPropertyName::relLeft].GetFloat();
1761 	const float &relRight = props[C4ScriptGuiWindowPropertyName::relRight].GetFloat();
1762 	const float &relTop = props[C4ScriptGuiWindowPropertyName::relTop].GetFloat();
1763 	const float &relBottom = props[C4ScriptGuiWindowPropertyName::relBottom].GetFloat();
1764 
1765 	// same for margins
1766 	const float &leftMargin = props[C4ScriptGuiWindowPropertyName::leftMargin].GetFloat();
1767 	const float &rightMargin = props[C4ScriptGuiWindowPropertyName::rightMargin].GetFloat();
1768 	const float &topMargin = props[C4ScriptGuiWindowPropertyName::topMargin].GetFloat();
1769 	const float &bottomMargin = props[C4ScriptGuiWindowPropertyName::bottomMargin].GetFloat();
1770 
1771 	const float &relLeftMargin = props[C4ScriptGuiWindowPropertyName::relLeftMargin].GetFloat();
1772 	const float &relRightMargin = props[C4ScriptGuiWindowPropertyName::relRightMargin].GetFloat();
1773 	const float &relTopMargin = props[C4ScriptGuiWindowPropertyName::relTopMargin].GetFloat();
1774 	const float &relBottomMargin = props[C4ScriptGuiWindowPropertyName::relBottomMargin].GetFloat();
1775 
1776 	// calculate drawing rectangle
1777 	float leftDrawX = relLeft * parentWidth + Em2Pix(left) + (Em2Pix(leftMargin) + relLeftMargin * parentWidth);
1778 	float rightDrawX = relRight * parentWidth + Em2Pix(right) - (Em2Pix(rightMargin) + relRightMargin * parentWidth);
1779 	float topDrawY = relTop * parentHeight + Em2Pix(top) + (Em2Pix(topMargin) + relTopMargin * parentHeight);
1780 	float bottomDrawY = relBottom * parentHeight + Em2Pix(bottom) - (Em2Pix(bottomMargin) + relBottomMargin * parentHeight);
1781 	float width = rightDrawX - leftDrawX;
1782 	float height = bottomDrawY - topDrawY;
1783 
1784 	rcBounds.x = leftDrawX;
1785 	rcBounds.y = topDrawY;
1786 	rcBounds.Wdt = width;
1787 	rcBounds.Hgt = height;
1788 
1789 	// If this window contains text, we auto-fit to the text height;
1790 	// but we only break text when the window /would/ crop it otherwise.
1791 	StdCopyStrBuf *strBuf = props[C4ScriptGuiWindowPropertyName::text].GetStrBuf();
1792 	int minRequiredTextHeight = 0;
1793 	if (strBuf && !(style & C4ScriptGuiWindowStyleFlag::NoCrop))
1794 	{
1795 		StdStrBuf sText;
1796 		const int32_t rawTextHeight = ::GraphicsResource.FontRegular.BreakMessage(strBuf->getData(), rcBounds.Wdt, &sText, true);
1797 		// enable auto scroll
1798 		if (rawTextHeight - 1 > rcBounds.Hgt)
1799 		{
1800 			// If we need to scroll, we will also have to add a scrollbar that takes up some width.
1801 			// Recalculate the actual height, taking into account the scrollbar.
1802 			// This is not done in the calculation earlier, because then e.g. a 2x1em field could not contain two letters
1803 			// but would immediately add a linebreak.
1804 			// In the case that this window auto-resizes (FitChildren), the small additional margin to the bottom should not matter much.
1805 			const int32_t actualTextHeight = ::GraphicsResource.FontRegular.BreakMessage(strBuf->getData(), rcBounds.Wdt - pScrollBar->rcBounds.Wdt, &sText, true);
1806 			minRequiredTextHeight = actualTextHeight;
1807 		}
1808 		else
1809 		{
1810 			// Otherwise, still set the minimum size to the text height (without scrollbar).
1811 			// This is necessary so that e.g. Style::FitChildren works properly with pure text windows.
1812 			minRequiredTextHeight = rawTextHeight;
1813 		}
1814 	}
1815 
1816 	UpdateChildLayout(cgo, width, height);
1817 
1818 	// update scroll bar
1819 	// C4GUI::ScrollWindow::UpdateOwnPos();
1820 
1821 	// special layout selected?
1822 	if (style & C4ScriptGuiWindowStyleFlag::GridLayout)
1823 		UpdateLayoutGrid();
1824 	else if (style & C4ScriptGuiWindowStyleFlag::TightGridLayout)
1825 		UpdateLayoutTightGrid();
1826 	else if (style & C4ScriptGuiWindowStyleFlag::VerticalLayout)
1827 		UpdateLayoutVertical();
1828 
1829 	// check if we need a scroll-bar
1830 	int32_t topMostChild = 0;
1831 	int32_t bottomMostChild = minRequiredTextHeight;
1832 	for (Element * element : *this)
1833 	{
1834 		C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1835 		const int32_t &childTop = child->rcBounds.y;
1836 		const int32_t childBottom = childTop + child->rcBounds.Hgt;
1837 		if (childTop < topMostChild) topMostChild = childTop;
1838 		if (childBottom > bottomMostChild) bottomMostChild = childBottom;
1839 	}
1840 	// do we need to adjust our size to fit the child windows?
1841 	if (style & C4ScriptGuiWindowStyleFlag::FitChildren)
1842 	{
1843 		rcBounds.Hgt = bottomMostChild;
1844 	}
1845 	// update scroll rectangle:
1846 	// (subtract one against rounding errors)
1847 	bottomMostChild = std::max(bottomMostChild, rcBounds.Hgt);
1848 	iClientHeight = bottomMostChild - topMostChild - 1;
1849 	C4GUI::ScrollWindow::Update();
1850 
1851 	pScrollBar->rcBounds.Wdt = C4GUI_ScrollBarWdt;
1852 	pScrollBar->rcBounds.x = rcBounds.Wdt - pScrollBar->rcBounds.Wdt;
1853 	pScrollBar->rcBounds.y = 0;
1854 	pScrollBar->rcBounds.Hgt = rcBounds.Hgt;
1855 	pScrollBar->Update();
1856 
1857 	// never show scrollbar on non-cropping windows
1858 	if ((style & C4ScriptGuiWindowStyleFlag::NoCrop) || !C4GUI::ScrollWindow::IsScrollingNecessary())
1859 		pScrollBar->SetVisibility(false);
1860 	// The "dirty" flag is unset here. Note that it's only used for non "multiple"-style windows after startup.
1861 	// The "multiple"-style windows are updated together when the root window does a full refresh.
1862 	mainWindowNeedsLayoutUpdate = false;
1863 	return true;
1864 }
1865 
DrawAll(C4TargetFacet & cgo,int32_t player)1866 bool C4ScriptGuiWindow::DrawAll(C4TargetFacet &cgo, int32_t player)
1867 {
1868 	assert(IsRoot()); // we are root
1869 	if (!IsVisible()) return false;
1870 	// if the viewport shows an upper-board, apply an offset to everything
1871 	const int oldTargetX = cgo.TargetX;
1872 	const int oldTargetY = cgo.TargetY;
1873 	cgo.TargetX += cgo.X;
1874 	cgo.TargetY += cgo.Y;
1875 	// this will check whether the viewport resized and we need an update
1876 	UpdateLayout(cgo);
1877 	// step one: draw all multiple-tagged windows
1878 	DrawChildren(cgo, player, 1);
1879 	// TODO: adjust rectangle for main menu if multiple windows exist
1880 	// step two: draw one "main" menu
1881 	DrawChildren(cgo, player, 0);
1882 	// ..and restore the offset
1883 	cgo.TargetX = oldTargetX;
1884 	cgo.TargetY = oldTargetY;
1885 	return true;
1886 }
1887 
Draw(C4TargetFacet & cgo,int32_t player,C4Rect * currentClippingRect)1888 bool C4ScriptGuiWindow::Draw(C4TargetFacet &cgo, int32_t player, C4Rect *currentClippingRect)
1889 {
1890 	assert(!IsRoot()); // not root, root needs to receive DrawAll
1891 
1892 	// message hidden?
1893 	if (!IsVisibleTo(player)) return false;
1894 
1895 	const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
1896 
1897 	if (mainWindowNeedsLayoutUpdate)
1898 	{
1899 		assert(GetParent() && (static_cast<C4ScriptGuiWindow*>(GetParent())->IsRoot()));
1900 		assert(isMainWindow);
1901 		assert(!(style & C4ScriptGuiWindowStyleFlag::Multiple) && "\"Multiple\"-style window not updated by a full refresh of the root window.");
1902 		C4ScriptGuiWindow * parent = static_cast<C4ScriptGuiWindow*>(GetParent());
1903 		UpdateLayout(cgo, parent->rcBounds.Wdt, parent->rcBounds.Hgt);
1904 		assert(!mainWindowNeedsLayoutUpdate);
1905 	}
1906 
1907 	const int32_t outDrawX = cgo.X + cgo.TargetX + rcBounds.x;
1908 	const int32_t outDrawY = cgo.Y + cgo.TargetY + rcBounds.y;
1909 	const int32_t outDrawWdt = rcBounds.Wdt;
1910 	const int32_t outDrawHgt = rcBounds.Hgt;
1911 	const int32_t outDrawRight = outDrawX + rcBounds.Wdt;
1912 	const int32_t outDrawBottom = outDrawY + rcBounds.Hgt;
1913 	// draw various properties
1914 	C4Facet cgoOut(cgo.Surface, outDrawX, outDrawY, outDrawWdt, outDrawHgt);
1915 
1916 	const int32_t &backgroundColor = props[C4ScriptGuiWindowPropertyName::backgroundColor].GetInt();
1917 	if (backgroundColor)
1918 		pDraw->DrawBoxDw(cgo.Surface, outDrawX, outDrawY, outDrawRight - 1.0f, outDrawBottom - 1.0f, backgroundColor);
1919 
1920 	C4GUI::FrameDecoration *frameDecoration = props[C4ScriptGuiWindowPropertyName::frameDecoration].GetFrameDecoration();
1921 
1922 	if (frameDecoration)
1923 	{
1924 		// the frame decoration will adjust for cgo.TargetX/Y itself
1925 		C4Rect rect(
1926 			outDrawX - frameDecoration->iBorderLeft - cgo.TargetX,
1927 			outDrawY - frameDecoration->iBorderTop - cgo.TargetY,
1928 			outDrawWdt + frameDecoration->iBorderRight + frameDecoration->iBorderLeft,
1929 			outDrawHgt + frameDecoration->iBorderBottom + frameDecoration->iBorderTop);
1930 		frameDecoration->Draw(cgo, rect);
1931 	}
1932 
1933 	C4Object *symbolObject = props[C4ScriptGuiWindowPropertyName::symbolObject].GetObject();
1934 	if (symbolObject)
1935 	{
1936 		symbolObject->DrawPicture(cgoOut, false, nullptr);
1937 	}
1938 	else
1939 	{
1940 		C4Def *symbolDef = props[C4ScriptGuiWindowPropertyName::symbolDef].GetDef();
1941 		StdCopyStrBuf *graphicsName = props[C4ScriptGuiWindowPropertyName::symbolGraphicsName].GetStrBuf();
1942 		if (symbolDef)
1943 		{
1944 			symbolDef->Draw(cgoOut, false, 0UL, nullptr, 0, 0, nullptr, graphicsName ? graphicsName->getData() : nullptr);
1945 		}
1946 	}
1947 
1948 	StdCopyStrBuf *strBuf = props[C4ScriptGuiWindowPropertyName::text].GetStrBuf();
1949 
1950 	if (strBuf)
1951 	{
1952 		StdStrBuf sText;
1953 		int alignment = ALeft;
1954 		// If we are showing a scrollbar, we need to leave a bit of space for it so that it doesn't overlap the text.
1955 		const int scrollbarXOffset = pScrollBar->IsVisible() ? pScrollBar->rcBounds.Wdt : 0;
1956 		const int scrollbarScroll = pScrollBar->IsVisible() ? this->GetScrollY() : 0;
1957 		const int actualDrawingWidth = outDrawWdt - scrollbarXOffset;
1958 
1959 		// If we are set to NoCrop, the message will be split by string-defined line breaks only.
1960 		int allowedTextWidth = actualDrawingWidth;
1961 
1962 		if (style & C4ScriptGuiWindowStyleFlag::NoCrop)
1963 			allowedTextWidth = std::numeric_limits<int>::max();
1964 		int32_t textHgt = ::GraphicsResource.FontRegular.BreakMessage(strBuf->getData(), allowedTextWidth, &sText, true);
1965 		float textYOffset = static_cast<float>(-scrollbarScroll), textXOffset = 0.0f;
1966 		if (style & C4ScriptGuiWindowStyleFlag::TextVCenter)
1967 			textYOffset = float(outDrawHgt) / 2.0f - float(textHgt) / 2.0f;
1968 		else if (style & C4ScriptGuiWindowStyleFlag::TextBottom)
1969 			textYOffset = float(outDrawHgt) - float(textHgt);
1970 		if (style & C4ScriptGuiWindowStyleFlag::TextHCenter)
1971 		{
1972 			int wdt, hgt;
1973 			::GraphicsResource.FontRegular.GetTextExtent(sText.getData(), wdt, hgt, true);
1974 			textXOffset = float(actualDrawingWidth) / 2.0f;
1975 			alignment = ACenter;
1976 		}
1977 		else if (style & C4ScriptGuiWindowStyleFlag::TextRight)
1978 		{
1979 			alignment = ARight;
1980 			int wdt, hgt;
1981 			::GraphicsResource.FontRegular.GetTextExtent(sText.getData(), wdt, hgt, true);
1982 			textXOffset = float(actualDrawingWidth);
1983 		}
1984 		pDraw->TextOut(sText.getData(), ::GraphicsResource.FontRegular, 1.0, cgo.Surface, outDrawX + textXOffset, outDrawY + textYOffset, 0xffffffff, alignment);
1985 	}
1986 
1987 
1988 	if (GraphicsSystem.ShowMenuInfo) // print helpful debug info
1989 	{
1990 		DWORD frameColor = C4RGB(100, 150, 100);
1991 		if (currentMouseState & MouseState::Focus) frameColor = C4RGB(0, 255, 0);
1992 
1993 		pDraw->DrawFrameDw(cgo.Surface, outDrawX, outDrawY, outDrawRight, outDrawBottom, frameColor);
1994 		if (target || id)
1995 		{
1996 			StdStrBuf buf = FormatString("%s(%d)", target ? target->GetName() : "", id);
1997 			pDraw->TextOut(buf.getData(), ::GraphicsResource.FontCaption, 1.0, cgo.Surface, cgo.X + outDrawRight, cgo.Y + outDrawBottom - ::GraphicsResource.FontCaption.GetLineHeight(), 0xffff00ff, ARight);
1998 		}
1999 		//StdStrBuf buf2 = FormatString("(%d, %d, %d, %d)", rcBounds.x, rcBounds.y, rcBounds.Wdt, rcBounds.Hgt);
2000 		//pDraw->TextOut(buf2.getData(), ::GraphicsResource.FontCaption, 1.0, cgo.Surface, cgo.X + outDrawX + rcBounds.Wdt / 2, cgo.Y + outDrawY + +rcBounds.Hgt / 2, 0xff00ffff, ACenter);
2001 	}
2002 
2003 	DrawChildren(cgo, player, -1, currentClippingRect);
2004 	return true;
2005 }
2006 
GetClippingRect(int32_t & left,int32_t & top,int32_t & right,int32_t & bottom)2007 bool C4ScriptGuiWindow::GetClippingRect(int32_t &left, int32_t &top, int32_t &right, int32_t &bottom)
2008 {
2009 	const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
2010 	if (IsRoot() || (style & C4ScriptGuiWindowStyleFlag::NoCrop))
2011 		return false;
2012 
2013 	// Other windows always clip their children.
2014 	// This implicitly clips childrens' text to the parent size, too.
2015 	left = rcBounds.x;
2016 	top = rcBounds.y;
2017 	right = rcBounds.Wdt + left;
2018 	bottom = rcBounds.Hgt + top;
2019 	return true;
2020 }
2021 
SetTag(C4String * tag)2022 void C4ScriptGuiWindow::SetTag(C4String *tag)
2023 {
2024 	// set tag on all properties
2025 	for (uint32_t i = 0; i < C4ScriptGuiWindowPropertyName::_lastProp; ++i)
2026 		if (props[i].SwitchTag(tag))
2027 		{
2028 			// only if tag could have changed position etc.
2029 			if (i <= C4ScriptGuiWindowPropertyName::relBottom || i == C4ScriptGuiWindowPropertyName::text || i == C4ScriptGuiWindowPropertyName::style || i == C4ScriptGuiWindowPropertyName::priority)
2030 				RequestLayoutUpdate();
2031 		}
2032 
2033 	// .. and children
2034 	for (C4GUI::Element * element : *this)
2035 		(static_cast<C4ScriptGuiWindow*>(element))->SetTag(tag);
2036 }
2037 
MouseEnter(C4GUI::CMouse &)2038 void C4ScriptGuiWindow::MouseEnter(C4GUI::CMouse &)
2039 {
2040 	assert(::MouseControl.GetPlayer() != NO_OWNER);
2041 }
2042 
OnMouseIn(int32_t player,int32_t parentOffsetX,int32_t parentOffsetY)2043 void C4ScriptGuiWindow::OnMouseIn(int32_t player, int32_t parentOffsetX, int32_t parentOffsetY)
2044 {
2045 	assert(!HasMouseFocus() && "custom menu window properly loaded incorrectly!");
2046 	currentMouseState = MouseState::Focus;
2047 
2048 	// no need to notify children, this is done in MouseInput
2049 
2050 	// update tooltip info if applicable
2051 	StdCopyStrBuf *strBuf = props[C4ScriptGuiWindowPropertyName::tooltip].GetStrBuf();
2052 	if (strBuf)
2053 	{
2054 		C4Viewport * viewport = ::Viewports.GetViewport(player);
2055 		if (viewport)
2056 		{
2057 			const float guiZoom = viewport->GetGUIZoom();
2058 			const float x = float(parentOffsetX + rcBounds.x) / guiZoom;
2059 			const float y = float(parentOffsetY + rcBounds.y) / guiZoom;
2060 			const float wdt = float(rcBounds.Wdt) / guiZoom;
2061 			const float hgt = float(rcBounds.Hgt) / guiZoom;
2062 			::MouseControl.SetTooltipRectangle(C4Rect(x, y, wdt, hgt));
2063 			::MouseControl.SetTooltipText(*strBuf);
2064 		}
2065 	}
2066 	// execute action
2067 	int32_t actionType = C4ScriptGuiWindowPropertyName::onMouseInAction;
2068 	C4ScriptGuiWindowAction *action = props[actionType].GetAction();
2069 	if (!action) return;
2070 	action->Execute(this, player, actionType);
2071 }
2072 
MouseLeave(C4GUI::CMouse &)2073 void C4ScriptGuiWindow::MouseLeave(C4GUI::CMouse &)
2074 {
2075 	assert(::MouseControl.GetPlayer() != NO_OWNER);
2076 
2077 }
OnMouseOut(int32_t player)2078 void C4ScriptGuiWindow::OnMouseOut(int32_t player)
2079 {
2080 	assert(HasMouseFocus() && "custom menu window probably loaded incorrectly!");
2081 	currentMouseState = MouseState::None;
2082 
2083 	// needs to notify children
2084 	for (C4GUI::Element *iter : *this)
2085 	{
2086 		C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(iter);
2087 		if (child->HasMouseFocus())
2088 			child->OnMouseOut(player);
2089 	}
2090 
2091 	// execute action
2092 	int32_t actionType = C4ScriptGuiWindowPropertyName::onMouseOutAction;
2093 	C4ScriptGuiWindowAction *action = props[actionType].GetAction();
2094 	if (!action) return;
2095 	action->Execute(this, player, actionType);
2096 }
2097 
MouseInput(int32_t button,int32_t mouseX,int32_t mouseY,DWORD dwKeyParam)2098 bool C4ScriptGuiWindow::MouseInput(int32_t button, int32_t mouseX, int32_t mouseY, DWORD dwKeyParam)
2099 {
2100 	// only called on root
2101 	assert(IsRoot());
2102 	// This is only called during a mouse move event, where the MouseControl's player is available.
2103 	const int32_t &player = ::MouseControl.GetPlayer();
2104 	assert(player != NO_OWNER);
2105 	// Only allow one window to catch the mouse input.
2106 	// Do not simply return, however, since other windows might need OnMouseOut().
2107 	bool oneActionAlreadyExecuted = false;
2108 	// non-multiple-windows have a higher priority
2109 	// this is important since they are also drawn on top
2110 	for (int withMultipleFlag = 0; withMultipleFlag <= 1; ++withMultipleFlag)
2111 	{
2112 		for (auto iter = rbegin(); iter != rend(); ++iter)
2113 		{
2114 			C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(*iter);
2115 
2116 			const int32_t &style = child->props[C4ScriptGuiWindowPropertyName::style].GetInt();
2117 			if ((withMultipleFlag == 0) && (style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
2118 			if ((withMultipleFlag == 1) && !(style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
2119 
2120 			// Do the visibility check first. The child itself won't do it, because we are handling mouse in/out here, too.
2121 			if (!child->IsVisibleTo(player)) continue;
2122 
2123 			// we are root, we have to adjust the position for the "main" windows that are centered
2124 			int32_t adjustedMouseX = 0, adjustedMouseY = mouseY;
2125 			int32_t offsetX = 0, offsetY = 0;
2126 			if (withMultipleFlag == 0)
2127 			{
2128 				offsetX = -rcBounds.x;
2129 				offsetY = -rcBounds.y;
2130 			}
2131 
2132 			adjustedMouseX = mouseX + offsetX;
2133 			adjustedMouseY = mouseY + offsetY;
2134 
2135 			int32_t childLeft = child->rcBounds.x;
2136 			int32_t childRight = child->rcBounds.x + child->rcBounds.Wdt;
2137 			int32_t childTop = child->rcBounds.y;
2138 			int32_t childBottom = child->rcBounds.y + child->rcBounds.Hgt;
2139 
2140 			bool inArea = true;
2141 			if ((adjustedMouseX < childLeft) || (adjustedMouseX > childRight)) inArea = false;
2142 			else if ((adjustedMouseY < childTop) || (adjustedMouseY > childBottom)) inArea = false;
2143 
2144 			if (!inArea) // notify child if it had mouse focus before
2145 			{
2146 				if (child->HasMouseFocus())
2147 					child->OnMouseOut(player);
2148 				continue;
2149 			}
2150 			// Don't break since some more OnMouseOut might be necessary
2151 			if (oneActionAlreadyExecuted) continue;
2152 
2153 
2154 			// keep the mouse coordinates relative to the child's bounds
2155 			if (child->ProcessMouseInput(button, adjustedMouseX - childLeft, adjustedMouseY - childTop - iScrollY, dwKeyParam, childLeft - offsetX, childTop + iScrollY - offsetY))
2156 			{
2157 				oneActionAlreadyExecuted = true;
2158 			}
2159 		}
2160 	}
2161 
2162 	return oneActionAlreadyExecuted;
2163 }
2164 
ProcessMouseInput(int32_t button,int32_t mouseX,int32_t mouseY,DWORD dwKeyParam,int32_t parentOffsetX,int32_t parentOffsetY)2165 bool C4ScriptGuiWindow::ProcessMouseInput(int32_t button, int32_t mouseX, int32_t mouseY, DWORD dwKeyParam, int32_t parentOffsetX, int32_t parentOffsetY)
2166 {
2167 	const int32_t &player = ::MouseControl.GetPlayer();
2168 	assert(player != NO_OWNER);
2169 
2170 	// completely ignore mouse if the appropriate flag is set
2171 	const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
2172 	if (style & C4ScriptGuiWindowStyleFlag::IgnoreMouse)
2173 		return false;
2174 
2175 	// we have mouse focus! Is this new?
2176 	if (!HasMouseFocus())
2177 		OnMouseIn(player, parentOffsetX, parentOffsetY);
2178 
2179 	// Make sure the UI does not catch release events without matching key-down events.
2180 	// Otherwise, you could e.g. open a menu on left-down and then the menu would block the left-up event, leading to issues.
2181 	if (button == C4MC_Button_LeftUp)
2182 	{
2183 		// Do not catch up-events without prior down-events!
2184 		if (!(currentMouseState & MouseState::MouseDown)) return false;
2185 	}
2186 
2187 	// do not simply break the loop since some OnMouseOut might go missing
2188 	bool oneActionAlreadyExecuted = false;
2189 
2190 	const int32_t scrollAdjustedMouseY = mouseY + iScrollY;
2191 
2192 	// children actually have a higher priority
2193 	bool overChild = false; // remember for later, catch all actions that are in theory over children, even if not reaction (if main window)
2194 	// use reverse iterator since children with higher Priority appear later in the list
2195 	for (auto iter = rbegin(); iter != rend(); ++iter)
2196 	{
2197 		C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(*iter);
2198 
2199 		// Do the visibility check first. The child itself won't do it, because we are handling mouse in/out here, too.
2200 		if (!child->IsVisibleTo(player)) continue;
2201 
2202 		const int32_t childLeft = child->rcBounds.x;
2203 		const int32_t childRight = child->rcBounds.x + child->rcBounds.Wdt;
2204 		const int32_t childTop = child->rcBounds.y;
2205 		const int32_t childBottom = child->rcBounds.y + child->rcBounds.Hgt;
2206 
2207 		bool inArea = true;
2208 		if ((mouseX <= childLeft) || (mouseX > childRight)) inArea = false;
2209 		else if ((scrollAdjustedMouseY <= childTop) || (scrollAdjustedMouseY > childBottom)) inArea = false;
2210 
2211 		if (!inArea) // notify child if it had mouse focus before
2212 		{
2213 			if (child->HasMouseFocus())
2214 				child->OnMouseOut(player);
2215 			continue;
2216 		}
2217 
2218 		if (oneActionAlreadyExecuted) continue;
2219 
2220 		overChild = true;
2221 		// keep coordinates relative to children
2222 		if (child->ProcessMouseInput(button, mouseX - childLeft, scrollAdjustedMouseY - childTop, dwKeyParam, parentOffsetX + rcBounds.x, parentOffsetY + rcBounds.y - iScrollY))
2223 		{
2224 			oneActionAlreadyExecuted = true;
2225 		}
2226 	}
2227 
2228 	if (oneActionAlreadyExecuted) return true;
2229 
2230 	//C4GUI::Element::MouseInput(rMouse, button, mouseX, mouseY, dwKeyParam);
2231 
2232 	// remember button-down events. The action will only be executed on button-up
2233 	// The sequence for double-clicks is LeftDown-LeftUp-LeftDouble-LeftUp, so treat double as down
2234 	if (button == C4MC_Button_LeftDown || button == C4MC_Button_LeftDouble)
2235 		currentMouseState |= MouseState::MouseDown;
2236 	// trigger!
2237 	if (button == C4MC_Button_LeftUp)
2238 	{
2239 		currentMouseState = currentMouseState & ~MouseState::MouseDown;
2240 		C4ScriptGuiWindowAction *action = props[C4ScriptGuiWindowPropertyName::onClickAction].GetAction();
2241 		if (action)
2242 		{
2243 			action->Execute(this, player, C4ScriptGuiWindowPropertyName::onClickAction);
2244 			return true;
2245 		}
2246 	}
2247 
2248 	// for scroll-enabled windows, scroll contents with wheel
2249 	if (pScrollBar->IsVisible() && (button == C4MC_Button_Wheel))
2250 	{
2251 		short delta = (short)(dwKeyParam >> 16);
2252 		ScrollBy(-delta);
2253 		//float fac = (lastDrawPosition.bottomMostChild - lastDrawPosition.topMostChild);
2254 		//if (fac == 0.0f) fac = 1.0f;
2255 		//scrollBar->ScrollBy(-float(delta) / fac);
2256 		return true;
2257 	}
2258 
2259 	// forward to scroll-bar if in area
2260 	if (pScrollBar->IsVisible())
2261 	{
2262 		if ((mouseX > pScrollBar->rcBounds.x && mouseX < pScrollBar->rcBounds.x + pScrollBar->rcBounds.Wdt)
2263 			&& (mouseY > pScrollBar->rcBounds.y && mouseY < pScrollBar->rcBounds.y + pScrollBar->rcBounds.Hgt))
2264 		{
2265 			C4GUI::CMouse mouse(mouseX, mouseY);
2266 			if (::MouseControl.IsLeftDown()) mouse.LDown = true;
2267 			pScrollBar->MouseInput(mouse, button, mouseX - pScrollBar->rcBounds.x, mouseY - pScrollBar->rcBounds.y, dwKeyParam);
2268 		}
2269 	}
2270 
2271 
2272 	// if the user still clicked on a menu - even if it didn't do anything, catch it
2273 	// but do that only on the top-level to not stop traversing other branches
2274 	if (isMainWindow)
2275 		return overChild;
2276 	return false;
2277 }
2278 
ExecuteCommand(int32_t actionID,int32_t player,int32_t subwindowID,int32_t actionType,C4Object * target)2279 bool C4ScriptGuiWindow::ExecuteCommand(int32_t actionID, int32_t player, int32_t subwindowID, int32_t actionType, C4Object *target)
2280 {
2281 	if (isMainWindow && subwindowID) // we are a main window! try a shortcut through the ID?
2282 	{
2283 		MenuDebugLogF("passing command... instance:%d, plr:%d, subwin:%d, type:%d [I am %d, MW]", actionID, player, subwindowID, actionType, id);
2284 		MenuDebugLogF("stats:");
2285 		MenuDebugLogF("active menus:\t%d", GetElementCount());
2286 		MenuDebugLogF("children ID map:\t%d", childrenIDMap.size());
2287 		// the reasoning for that shortcut is that I assume that usually windows with actions will also have an ID assigned
2288 		// this obviously doesn't have to be the case, but I believe it's worth the try
2289 		std::pair<std::multimap<int32_t, C4ScriptGuiWindow*>::iterator, std::multimap<int32_t, C4ScriptGuiWindow*>::iterator> range;
2290 		range = childrenIDMap.equal_range(subwindowID);
2291 
2292 		for (std::multimap<int32_t, C4ScriptGuiWindow*>::iterator iter = range.first; iter != range.second; ++iter)
2293 		{
2294 			if (iter->second->ExecuteCommand(actionID, player, subwindowID, actionType, target))
2295 			{
2296 				MenuDebugLogF("shortcutting command sucessful!");
2297 				return true;
2298 			}
2299 		}
2300 		// it is not possible that another window would match the criteria. Abort later after self-check
2301 		MenuDebugLogF("shortcutting command failed.. no appropriate window");
2302 	}
2303 
2304 	// are we elligible?
2305 	if ((id == subwindowID) && (this->target == target))
2306 	{
2307 		MenuDebugLogF("stats: (I am %d)", id);
2308 		MenuDebugLogF("children:\t%d", GetElementCount());
2309 		MenuDebugLogF("all actions:\t%d", props[actionType].GetAllActions().size());
2310 		std::list<C4ScriptGuiWindowAction*> allActions = props[actionType].GetAllActions();
2311 		for (auto action : allActions)
2312 		{
2313 			assert(action && "C4ScriptGuiWindowProperty::GetAllActions returned list with null-pointer");
2314 
2315 			if (action->ExecuteCommand(actionID, this, player))
2316 			{
2317 				MenuDebugLogF("executing command sucessful!");
2318 				return true;
2319 			}
2320 		}
2321 
2322 		// note that we should not simply return false here
2323 		// there is no guarantee that only one window with that target&ID exists
2324 	}
2325 
2326 	// not caught, forward to children!
2327 	// abort if main window, though. See above
2328 	if (isMainWindow && subwindowID)
2329 	{
2330 		MenuDebugLogF("executing command failed!");
2331 		return false;
2332 	}
2333 
2334 	// otherwise, just pass to children..
2335 	for (C4GUI::Element *element : *this)
2336 	{
2337 		C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
2338 		if (child->ExecuteCommand(actionID, player, subwindowID, actionType, target))
2339 		{
2340 			MenuDebugLogF("passing command sucessful! (I am %d - &p)", id, this->target);
2341 			return true;
2342 		}
2343 	}
2344 	return false;
2345 }
2346 
IsRoot()2347 bool C4ScriptGuiWindow::IsRoot()
2348 {
2349 	return this == Game.ScriptGuiRoot.get();
2350 }
2351 
IsVisibleTo(int32_t player)2352 bool C4ScriptGuiWindow::IsVisibleTo(int32_t player)
2353 {
2354 	// Not visible at all?
2355 	if (!IsVisible()) return false;
2356 	// We have a player assigned and it's a different one?
2357 	const int32_t &myPlayer = props[C4ScriptGuiWindowPropertyName::player].GetInt();
2358 	if (myPlayer != ANY_OWNER && player != myPlayer) return false;
2359 	// We have a target object which is invisible to the player?
2360 	if (target && !target->IsVisible(player, false)) return false;
2361 	// Default to visible!
2362 	return true;
2363 }
2364