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 = [¤tX, ¤tY, &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