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