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 "control/C4Control.h"
33 #include "game/C4Application.h"
34 #include "game/C4GraphicsSystem.h"
35 #include "game/C4Viewport.h"
36 #include "graphics/C4Draw.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 
54 float C4ScriptGuiWindow::Em2Pix(float em)
55 {
56  return static_cast<float>(::GraphicsResource.FontRegular.GetFontHeight()) * em;
57 }
58 
59 float C4ScriptGuiWindow::Pix2Em(float pix)
60 {
61  return pix / static_cast<float>(std::max<int32_t>(1, ::GraphicsResource.FontRegular.GetFontHeight()));
62 }
63 
65 {
66  if (text)
67  text->DecRef();
68  if (nextAction)
69  delete nextAction;
70 }
71 
73 {
74  C4ValueArray *array = new C4ValueArray();
75 
76  switch (action)
77  {
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 
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 
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 }
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  {
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 
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 
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  {
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 
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 
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 
286 {
287  // is cleaned up from destructor of C4ScriptGuiWindow
288 }
289 
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 }
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 }
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 
312 void C4ScriptGuiWindowProperty::CleanUp(Prop &prop)
313 {
314  switch (type)
315  {
317  if (prop.deco) delete prop.deco;
318  break;
323  if (prop.action) delete prop.action;
324  break;
328  if (prop.strBuf) delete prop.strBuf;
329  break;
330  default:
331  break;
332  }
333 }
334 
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 
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  {
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 
388  val = C4Value(prop.d);
389  break;
390 
392  val = C4Value(prop.obj);
393  break;
394 
396  val = C4Value(prop.def);
397  break;
398 
400  if (prop.deco)
401  val = C4Value(prop.deco->pSourceDef);
402  break;
403 
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 
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 
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  {
496  assert (false && "Trying to set positional properties directly. Those should always come parsed from a string.");
497  break;
498 
502  current->d = value.getInt();
503  break;
504 
506  if (value == C4VNull)
507  current->d = ANY_OWNER;
508  else
509  current->d = value.getInt();
510  break;
511 
513  {
514  C4PropList *symbol = value.getPropList();
515  if (symbol)
516  current->obj = symbol->GetObject();
517  else current->obj = nullptr;
518  break;
519  }
521  {
522  C4PropList *symbol = value.getPropList();
523  if (symbol)
524  current->def = symbol->GetDef();
525  else current->def = nullptr;
526  break;
527  }
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  }
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  }
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 
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  {
585  if (taggedProperty.second.obj == pObj)
586  taggedProperty.second.obj = nullptr;
587  break;
588 
593  if (taggedProperty.second.action)
594  taggedProperty.second.action->ClearPointers(pObj);
595  break;
596  default:
597  return;
598  }
599  }
600 }
601 
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 
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 
625 {
626  Init();
627 }
628 
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
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
662  // other properties are 0
667  props[C4ScriptGuiWindowPropertyName::text].SetNull();
674  props[C4ScriptGuiWindowPropertyName::style].SetNull();
677 
678  wasRemoved = false;
679  closeActionWasExecuted = false;
681  target = nullptr;
682  pScrollBar->fAutoHide = true;
683 
684  rcBounds.x = rcBounds.y = 0;
685  rcBounds.Wdt = rcBounds.Hgt = 0;
686 }
687 
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
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 relative, absolute;
724  switch (i)
725  {
726  case 0:
729  break;
730  case 1:
733  break;
734  case 2:
737  break;
738  case 3:
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 
758 C4Value C4ScriptGuiWindow::MarginsToC4Value()
759 {
760  C4ValueArray *array = new C4ValueArray();
761  array->SetSize(4);
762 
767 
768  return C4Value(array);
769 }
770 
771 // helper function
772 void C4ScriptGuiWindow::SetPositionStringProperties(const C4Value &property, C4ScriptGuiWindowPropertyName relative, C4ScriptGuiWindowPropertyName 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
839 C4Value C4ScriptGuiWindow::PositionToC4Value(C4ScriptGuiWindowPropertyName relativeName, C4ScriptGuiWindowPropertyName 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 
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 
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,
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); }
934 #undef PROPERTY_TUPLE
935  break;
936  }
937  case P_Margin: val = MarginsToC4Value(); break;
940  case P_Symbol:
941  // either object or def
943  if (val == C4Value()) // is nil?
945  break;
946  case P_Target: val = C4Value(target); break;
947  case P_Text: val = props[C4ScriptGuiWindowPropertyName::text].ToC4Value(); break;
949  case P_Tooltip: val = props[C4ScriptGuiWindowPropertyName::tooltip].ToC4Value(); break;
950  case P_ID: val = C4Value(id); break;
955  case P_Style: val = props[C4ScriptGuiWindowPropertyName::style].ToC4Value(); break;
956  case P_Mode: val = C4Value(int32_t(currentMouseState)); 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 
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)
1154 
1155  if (resetStdTag || isLoading)
1156  SetTag(stdTag);
1157 
1158  return true;
1159 }
1160 
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!
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 
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 
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 
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 
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 
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 
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 
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 
1291 void C4ScriptGuiWindow::RemoveChild(C4ScriptGuiWindow *child, bool close, bool all)
1292 {
1293  // do a layout update asap
1294  if (!all && !IsRoot())
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)
1322 }
1323 
1325 {
1326  RemoveChild(nullptr, close, true);
1327 }
1328 
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
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 
1356 void C4ScriptGuiWindow::EnableScrollBar(bool enable, float childrenHeight)
1357 {
1358  const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
1359 
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)
1373  return;
1374  }
1375 
1376  if (style & C4ScriptGuiWindowStyleFlag::NoCrop) return;
1377 
1379 }
1380 
1381 
1382 float C4ScriptGuiWindow::CalculateRelativeSize(float parentWidthOrHeight, C4ScriptGuiWindowPropertyName absoluteProperty, C4ScriptGuiWindowPropertyName relativeProperty)
1383 {
1384  const float widthOrHeight = Em2Pix(props[absoluteProperty].GetFloat())
1385  + float(parentWidthOrHeight) * props[relativeProperty].GetFloat();
1386  return widthOrHeight;
1387 }
1388 
1389 
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  const float childLeftMargin = child->CalculateRelativeSize(width, leftMargin, relLeftMargin);
1406  const float childTopMargin = child->CalculateRelativeSize(height, topMargin, relTopMargin);
1407  const float childRightMargin = child->CalculateRelativeSize(width, rightMargin, relRightMargin);
1408  const float childBottomMargin = child->CalculateRelativeSize(height, bottomMargin, relBottomMargin);
1409 
1410  const float childWdtF = float(child->rcBounds.Wdt) + childLeftMargin + childRightMargin;
1411  const float childHgtF = float(child->rcBounds.Hgt) + childTopMargin + childBottomMargin;
1412 
1413  auto doLineBreak = [&]()
1414  {
1415  currentX = borderX;
1416  currentY += maxChildHeight + borderY;
1417  maxChildHeight = 0;
1418  };
1419 
1420  // do all the rounding after the calculations
1421  const auto childWdt = (int32_t)(childWdtF + 0.5f);
1422  const auto childHgt = (int32_t)(childHgtF + 0.5f);
1423 
1424  // Check if the child even fits in the remainder of the row
1425  const bool fitsInRow = (width - currentX) >= childWdt;
1426  if (!fitsInRow) doLineBreak();
1427 
1428  // remember the highest child to make sure rows don't overlap
1429  if (!maxChildHeight || (childHgt > maxChildHeight))
1430  {
1431  maxChildHeight = childHgt;
1432  lowestChildRelY = currentY + childHgt;
1433  }
1434  child->rcBounds.x = currentX + static_cast<int32_t>(childLeftMargin);
1435  child->rcBounds.y = currentY + static_cast<int32_t>(childTopMargin);
1436 
1437  currentX += childWdt + borderX;
1438  }
1439 
1440  // do we need a scroll bar?
1441  EnableScrollBar(lowestChildRelY > height, lowestChildRelY);
1442 }
1443 
1444 // Similar to the grid layout but tries to fill spaces more thoroughly.
1445 // It's slower and might reorder items.
1447 {
1448  const int32_t &width = rcBounds.Wdt;
1449  const int32_t &height = rcBounds.Hgt;
1450  const int32_t borderX(0), borderY(0);
1451  int32_t lowestChildRelY = 0;
1452 
1453  std::list<C4ScriptGuiWindow*> alreadyPlacedChildren;
1454 
1455  for (C4GUI::Element * element : *this)
1456  {
1457  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1458  // calculate the space the child needs, correctly respecting the margins
1459  const float childLeftMargin = child->CalculateRelativeSize(width, leftMargin, relLeftMargin);
1460  const float childTopMargin = child->CalculateRelativeSize(height, topMargin, relTopMargin);
1461  const float childRightMargin = child->CalculateRelativeSize(width, rightMargin, relRightMargin);
1462  const float childBottomMargin = child->CalculateRelativeSize(height, bottomMargin, relBottomMargin);
1463 
1464  const float childWdtF = float(child->rcBounds.Wdt) + childLeftMargin + childRightMargin;
1465  const float childHgtF = float(child->rcBounds.Hgt) + childTopMargin + childBottomMargin;
1466 
1467  // do all the rounding after the calculations
1468  const auto childWdt = (int32_t)(childWdtF + 0.5f);
1469  const auto childHgt = (int32_t)(childHgtF + 0.5f);
1470 
1471  // Look for a free spot.
1472  int32_t currentX = borderX;
1473  int32_t currentY = borderY;
1474 
1475  bool hadOverlap = false;
1476  int overlapRepeats = 0;
1477  do
1478  {
1479  auto overlapsWithOther = [&currentX, &currentY, &childWdt, &childHgt](C4ScriptGuiWindow *other)
1480  {
1481  if (currentX + childWdt <= other->rcBounds.x) return false;
1482  if (currentY + childHgt <= other->rcBounds.y) return false;
1483  if (currentX >= other->rcBounds.GetRight()) return false;
1484  if (currentY >= other->rcBounds.GetBottom()) return false;
1485  return true;
1486  };
1487 
1488  int32_t currentMinY = 0;
1489  hadOverlap = false;
1490  for (auto &other : alreadyPlacedChildren)
1491  {
1492  // Check if the other element is not yet above the new child.
1493  if ((other->rcBounds.GetBottom() > currentY) && other->rcBounds.Hgt > 0)
1494  {
1495  if (currentMinY == 0 || (other->rcBounds.GetBottom() < currentMinY))
1496  currentMinY = other->rcBounds.GetBottom();
1497  }
1498  // If overlapping, we must advance.
1499  if (overlapsWithOther(other))
1500  {
1501  hadOverlap = true;
1502  currentX = other->rcBounds.GetRight();
1503  // Break line if the element doesn't fit anymore.
1504  if (currentX + childWdt > width)
1505  {
1506  currentX = borderX;
1507  // Start forcing change once we start repeating the check. Otherwise, there might
1508  // be a composition of children that lead to infinite loop. The worst-case number
1509  // of sensible checks might O(N^2) be with a really unfortunate children list.
1510  const int32_t forcedMinimalChange = (overlapRepeats > alreadyPlacedChildren.size()) ? 1 : 0;
1511  currentY = std::max(currentY + forcedMinimalChange, currentMinY);
1512  }
1513  }
1514  }
1515  overlapRepeats += 1;
1516  } while (hadOverlap);
1517 
1518  alreadyPlacedChildren.push_back(child);
1519 
1520  lowestChildRelY = std::max(lowestChildRelY, currentY + childHgt);
1521  child->rcBounds.x = currentX + static_cast<int32_t>(childLeftMargin);
1522  child->rcBounds.y = currentY + static_cast<int32_t>(childTopMargin);
1523  }
1524 
1525  // do we need a scroll bar?
1526  EnableScrollBar(lowestChildRelY > height, lowestChildRelY);
1527 }
1528 
1530 {
1531  const int32_t borderY(0);
1532  int32_t currentY = borderY;
1533 
1534  for (C4GUI::Element * element : *this)
1535  {
1536  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1537 
1538  // Do the calculations in floats first to not lose accuracy.
1539  // Take the height of the child and then add the margins.
1540  const float childTopMargin = child->CalculateRelativeSize(rcBounds.Hgt, topMargin, relTopMargin);
1541  const float childBottomMargin = child->CalculateRelativeSize(rcBounds.Hgt, bottomMargin, relBottomMargin);
1542 
1543  const float childHgtF = float(child->rcBounds.Hgt) + childTopMargin + childBottomMargin;
1544  const int32_t childHgt = (int32_t)(childHgtF + 0.5f);
1545 
1546  child->rcBounds.y = currentY + childTopMargin;
1547  currentY += childHgt + borderY;
1548  }
1549 
1550  // do we need a scroll bar?
1551  EnableScrollBar(currentY > rcBounds.Hgt, currentY);
1552 }
1553 
1554 bool C4ScriptGuiWindow::DrawChildren(C4TargetFacet &cgo, int32_t player, int32_t withMultipleFlag, C4Rect *currentClippingRect)
1555 {
1556  // remember old target rectangle and adjust
1557  float oldTargetX = cgo.TargetX;
1558  float oldTargetY = cgo.TargetY;
1559  C4Rect myClippingRect;
1560  if (IsRoot())
1561  {
1562  cgo.TargetX = 0;
1563  cgo.TargetY = 0;
1565  // default: full screen clipper
1566  myClippingRect = C4Rect(0, 0, cgo.Wdt * cgo.Zoom, cgo.Hgt * cgo.Zoom);
1567  currentClippingRect = &myClippingRect;
1568  }
1569 
1570  // if ANY PARENT has scroll bar, then adjust clipper
1571  int32_t clipX1(0), clipX2(0), clipY1(0), clipY2(0);
1572  bool clipping = GetClippingRect(clipX1, clipY1, clipX2, clipY2);
1573 
1574  const int32_t targetClipX1 = cgo.X + cgo.TargetX + clipX1;
1575  const int32_t targetClipY1 = cgo.Y + cgo.TargetY + clipY1;
1576  const int32_t targetClipX2 = cgo.X + cgo.TargetX + clipX2;
1577  const int32_t targetClipY2 = cgo.Y + cgo.TargetY + clipY2;
1578 
1579  if (clipping)
1580  {
1581  // Take either the parent rectangle or restrict it additionally by the child's geometry.
1582  myClippingRect = C4Rect(
1583  std::max(currentClippingRect->x, targetClipX1),
1584  std::max(currentClippingRect->y, targetClipY1),
1585  std::min(currentClippingRect->Wdt, targetClipX2),
1586  std::min(currentClippingRect->Hgt, targetClipY2));
1587  currentClippingRect = &myClippingRect;
1588  }
1589 
1590  if (withMultipleFlag != 1)
1591  {
1592  cgo.TargetX += rcBounds.x;
1593  cgo.TargetY += rcBounds.y - iScrollY;
1594  }
1595  else
1596  {
1597  assert(IsRoot());
1598  assert(withMultipleFlag == 1);
1599  }
1600 
1601 
1602  // note that withMultipleFlag only plays a roll for the root-menu
1603  bool oneDrawn = false; // was at least one child drawn?
1604  //for (auto iter = rbegin(); iter != rend(); ++iter)
1605  for (auto element : *this)
1606  {
1607  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1608 
1609  if (withMultipleFlag != -1)
1610  {
1611  const int32_t &style = child->props[C4ScriptGuiWindowPropertyName::style].GetInt();
1612  if ((withMultipleFlag == 0) && (style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
1613  if ((withMultipleFlag == 1) && !(style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
1614  }
1615 
1616  pDraw->SetPrimaryClipper(currentClippingRect->x, currentClippingRect->y, currentClippingRect->Wdt, currentClippingRect->Hgt);
1617 
1618  if (child->Draw(cgo, player, currentClippingRect))
1619  oneDrawn = true;
1620  // draw only one window when drawing non-Multiple windows
1621  if (oneDrawn && (withMultipleFlag == 0)) break;
1622  }
1623 
1624  // Scrolling obviously does not affect the scroll bar.
1625  cgo.TargetY += iScrollY;
1626  // The scroll bar does not correct for the cgo offset (i.e. the upper board).
1627  cgo.TargetX += cgo.X;
1628  cgo.TargetY += cgo.Y;
1629 
1630  if (pScrollBar->IsVisible())
1631  pScrollBar->DrawElement(cgo);
1632 
1633  if (IsRoot())
1634  {
1636  }
1637 
1638  // restore target rectangle
1639  cgo.TargetX = oldTargetX;
1640  cgo.TargetY = oldTargetY;
1641  return oneDrawn;
1642 }
1643 
1645 {
1646  // directly requested on the root window?
1647  // That usually comes from another part of the engine (f.e. C4Viewport::RecalculateViewports) or from a multiple-window child
1648  if (!GetParent())
1649  {
1650  mainWindowNeedsLayoutUpdate = true;
1651  return;
1652  }
1653 
1654  // are we a direct child of the root?
1655  if (isMainWindow)
1656  {
1657  const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
1658 
1659  if (!(style & C4ScriptGuiWindowStyleFlag::Multiple)) // are we a simple centered window?
1660  {
1661  mainWindowNeedsLayoutUpdate = true;
1662  return;
1663  }
1664  else // we are one of the multiple windows.. the root better do a full refresh
1665  {}
1666  }
1667  // propagate to parent window
1668  static_cast<C4ScriptGuiWindow*>(GetParent())->RequestLayoutUpdate();
1669 }
1670 
1671 bool C4ScriptGuiWindow::UpdateChildLayout(C4TargetFacet &cgo, float parentWidth, float parentHeight)
1672 {
1673  for (Element * element : *this)
1674  {
1675  C4ScriptGuiWindow *window = static_cast<C4ScriptGuiWindow*>(element);
1676  window->UpdateLayout(cgo, parentWidth, parentHeight);
1677  }
1678  return true;
1679 }
1680 
1682 {
1683  assert(IsRoot()); // we are root
1684 
1685  // assume I am the root and use the a special rectangle in the viewport for drawing
1686  const float fullWidth = cgo.Wdt * cgo.Zoom - cgo.X;
1687  const float fullHeight = cgo.Hgt * cgo.Zoom - cgo.Y;
1688 
1689  // golden ratio defined default size!
1690  const float &targetWidthEm = C4ScriptGuiWindow::standardWidth;
1691  const float &targetHeightEm = C4ScriptGuiWindow::standardHeight;
1692 
1693  // adjust by viewport size
1694  const float minMarginPx = 50.0f;
1695  const float targetWidthPx = std::min(Em2Pix(targetWidthEm), fullWidth - 2.0f * minMarginPx);
1696  const float targetHeightPx = std::min(Em2Pix(targetHeightEm), fullHeight - 2.0f * minMarginPx);
1697 
1698  // calculate margins to center the window
1699  const float marginLeftRight = (fullWidth - targetWidthPx) / 2.0f;
1700  const float marginTopBottom = (fullHeight- targetHeightPx) / 2.0f;
1701 
1702  // we can only position the window by adjusting left/right/top/bottom
1703  const float &left = marginLeftRight;
1704  const float right = -marginLeftRight;
1705  const float &top = marginTopBottom;
1706  const float bottom = -marginTopBottom;
1707 
1708  // actual size, calculated from borders
1709  const float wdt = fullWidth - left + right;
1710  const float hgt = fullHeight - top + bottom;
1711 
1712  const bool needUpdate = mainWindowNeedsLayoutUpdate || (rcBounds.Wdt != int32_t(wdt)) || (rcBounds.Hgt != int32_t(hgt));
1713 
1714  if (needUpdate)
1715  {
1716  mainWindowNeedsLayoutUpdate = false;
1717 
1718  // these are the coordinates for the centered non-multiple windows
1719  rcBounds.x = static_cast<int>(left);
1720  rcBounds.y = static_cast<int>(top);
1721  rcBounds.Wdt = wdt;
1722  rcBounds.Hgt = hgt;
1723 
1724  // first update all multiple windows (that can cover the whole screen)
1725  for (Element * element : *this)
1726  {
1727  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1728  const int32_t &style = child->props[C4ScriptGuiWindowPropertyName::style].GetInt();
1729  if (!(style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
1730  child->UpdateLayout(cgo, fullWidth, fullHeight);
1731  }
1732  // then update all "main" windows in the center of the screen
1733  // todo: adjust the size of the main window based on the border-windows drawn before
1734  for (Element * element : *this)
1735  {
1736  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1737  const int32_t &style = child->props[C4ScriptGuiWindowPropertyName::style].GetInt();
1738  if ((style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
1739  child->UpdateLayout(cgo, wdt, hgt);
1740  }
1741 
1742  pScrollBar->SetVisibility(false);
1743  }
1744  return true;
1745 }
1746 
1747 bool C4ScriptGuiWindow::UpdateLayout(C4TargetFacet &cgo, float parentWidth, float parentHeight)
1748 {
1749  // fetch style
1750  const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
1751  // fetch current position as shortcut for overview
1752  const float &left = props[C4ScriptGuiWindowPropertyName::left].GetFloat();
1753  const float &right = props[C4ScriptGuiWindowPropertyName::right].GetFloat();
1754  const float &top = props[C4ScriptGuiWindowPropertyName::top].GetFloat();
1755  const float &bottom = props[C4ScriptGuiWindowPropertyName::bottom].GetFloat();
1756 
1759  const float &relTop = props[C4ScriptGuiWindowPropertyName::relTop].GetFloat();
1761 
1762  // same for margins
1767 
1772 
1773  // calculate drawing rectangle
1774  float leftDrawX = relLeft * parentWidth + Em2Pix(left) + (Em2Pix(leftMargin) + relLeftMargin * parentWidth);
1775  float rightDrawX = relRight * parentWidth + Em2Pix(right) - (Em2Pix(rightMargin) + relRightMargin * parentWidth);
1776  float topDrawY = relTop * parentHeight + Em2Pix(top) + (Em2Pix(topMargin) + relTopMargin * parentHeight);
1777  float bottomDrawY = relBottom * parentHeight + Em2Pix(bottom) - (Em2Pix(bottomMargin) + relBottomMargin * parentHeight);
1778  float width = rightDrawX - leftDrawX;
1779  float height = bottomDrawY - topDrawY;
1780 
1781  rcBounds.x = leftDrawX;
1782  rcBounds.y = topDrawY;
1783  rcBounds.Wdt = width;
1784  rcBounds.Hgt = height;
1785 
1786  // If this window contains text, we auto-fit to the text height;
1787  // but we only break text when the window /would/ crop it otherwise.
1789  int minRequiredTextHeight = 0;
1790  if (strBuf && !(style & C4ScriptGuiWindowStyleFlag::NoCrop))
1791  {
1792  StdStrBuf sText;
1793  const int32_t rawTextHeight = ::GraphicsResource.FontRegular.BreakMessage(strBuf->getData(), rcBounds.Wdt, &sText, true);
1794  // enable auto scroll
1795  if (rawTextHeight - 1 > rcBounds.Hgt)
1796  {
1797  // If we need to scroll, we will also have to add a scrollbar that takes up some width.
1798  // Recalculate the actual height, taking into account the scrollbar.
1799  // This is not done in the calculation earlier, because then e.g. a 2x1em field could not contain two letters
1800  // but would immediately add a linebreak.
1801  // In the case that this window auto-resizes (FitChildren), the small additional margin to the bottom should not matter much.
1802  const int32_t actualTextHeight = ::GraphicsResource.FontRegular.BreakMessage(strBuf->getData(), rcBounds.Wdt - pScrollBar->rcBounds.Wdt, &sText, true);
1803  minRequiredTextHeight = actualTextHeight;
1804  }
1805  else
1806  {
1807  // Otherwise, still set the minimum size to the text height (without scrollbar).
1808  // This is necessary so that e.g. Style::FitChildren works properly with pure text windows.
1809  minRequiredTextHeight = rawTextHeight;
1810  }
1811  }
1812 
1813  UpdateChildLayout(cgo, width, height);
1814 
1815  // update scroll bar
1816  // C4GUI::ScrollWindow::UpdateOwnPos();
1817 
1818  // special layout selected?
1820  UpdateLayoutGrid();
1825 
1826  // check if we need a scroll-bar
1827  int32_t topMostChild = 0;
1828  int32_t bottomMostChild = minRequiredTextHeight;
1829  for (Element * element : *this)
1830  {
1831  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1832  const int32_t &childTop = child->rcBounds.y;
1833  const int32_t childBottom = childTop + child->rcBounds.Hgt;
1834  if (childTop < topMostChild) topMostChild = childTop;
1835  if (childBottom > bottomMostChild) bottomMostChild = childBottom;
1836  }
1837  // do we need to adjust our size to fit the child windows?
1838  if (style & C4ScriptGuiWindowStyleFlag::FitChildren)
1839  {
1840  rcBounds.Hgt = bottomMostChild;
1841  }
1842  // update scroll rectangle:
1843  // (subtract one against rounding errors)
1844  bottomMostChild = std::max(bottomMostChild, rcBounds.Hgt);
1845  iClientHeight = bottomMostChild - topMostChild - 1;
1847 
1850  pScrollBar->rcBounds.y = 0;
1852  pScrollBar->Update();
1853 
1854  // never show scrollbar on non-cropping windows
1855  if ((style & C4ScriptGuiWindowStyleFlag::NoCrop) || !C4GUI::ScrollWindow::IsScrollingNecessary())
1856  pScrollBar->SetVisibility(false);
1857  // The "dirty" flag is unset here. Note that it's only used for non "multiple"-style windows after startup.
1858  // The "multiple"-style windows are updated together when the root window does a full refresh.
1859  mainWindowNeedsLayoutUpdate = false;
1860  return true;
1861 }
1862 
1864 {
1865  assert(IsRoot()); // we are root
1866  if (!IsVisible()) return false;
1867  // if the viewport shows an upper-board, apply an offset to everything
1868  const int oldTargetX = cgo.TargetX;
1869  const int oldTargetY = cgo.TargetY;
1870  cgo.TargetX += cgo.X;
1871  cgo.TargetY += cgo.Y;
1872  // this will check whether the viewport resized and we need an update
1873  UpdateLayout(cgo);
1874  // step one: draw all multiple-tagged windows
1875  DrawChildren(cgo, player, 1);
1876  // TODO: adjust rectangle for main menu if multiple windows exist
1877  // step two: draw one "main" menu
1878  DrawChildren(cgo, player, 0);
1879  // ..and restore the offset
1880  cgo.TargetX = oldTargetX;
1881  cgo.TargetY = oldTargetY;
1882  return true;
1883 }
1884 
1885 bool C4ScriptGuiWindow::Draw(C4TargetFacet &cgo, int32_t player, C4Rect *currentClippingRect)
1886 {
1887  assert(!IsRoot()); // not root, root needs to receive DrawAll
1888 
1889  // message hidden?
1890  if (!IsVisibleTo(player)) return false;
1891 
1892  const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
1893 
1894  if (mainWindowNeedsLayoutUpdate)
1895  {
1896  assert(GetParent() && (static_cast<C4ScriptGuiWindow*>(GetParent())->IsRoot()));
1897  assert(isMainWindow);
1898  assert(!(style & C4ScriptGuiWindowStyleFlag::Multiple) && "\"Multiple\"-style window not updated by a full refresh of the root window.");
1899  C4ScriptGuiWindow * parent = static_cast<C4ScriptGuiWindow*>(GetParent());
1900  UpdateLayout(cgo, parent->rcBounds.Wdt, parent->rcBounds.Hgt);
1901  assert(!mainWindowNeedsLayoutUpdate);
1902  }
1903 
1904  const int32_t outDrawX = cgo.X + cgo.TargetX + rcBounds.x;
1905  const int32_t outDrawY = cgo.Y + cgo.TargetY + rcBounds.y;
1906  const int32_t outDrawWdt = rcBounds.Wdt;
1907  const int32_t outDrawHgt = rcBounds.Hgt;
1908  const int32_t outDrawRight = outDrawX + rcBounds.Wdt;
1909  const int32_t outDrawBottom = outDrawY + rcBounds.Hgt;
1910  // draw various properties
1911  C4Facet cgoOut(cgo.Surface, outDrawX, outDrawY, outDrawWdt, outDrawHgt);
1912 
1914  if (backgroundColor)
1915  pDraw->DrawBoxDw(cgo.Surface, outDrawX, outDrawY, outDrawRight - 1.0f, outDrawBottom - 1.0f, backgroundColor);
1916 
1918 
1919  if (frameDecoration)
1920  {
1921  // the frame decoration will adjust for cgo.TargetX/Y itself
1922  C4Rect rect(
1923  outDrawX - frameDecoration->iBorderLeft - cgo.TargetX,
1924  outDrawY - frameDecoration->iBorderTop - cgo.TargetY,
1925  outDrawWdt + frameDecoration->iBorderRight + frameDecoration->iBorderLeft,
1926  outDrawHgt + frameDecoration->iBorderBottom + frameDecoration->iBorderTop);
1927  frameDecoration->Draw(cgo, rect);
1928  }
1929 
1931  if (symbolObject)
1932  {
1933  symbolObject->DrawPicture(cgoOut, false, nullptr);
1934  }
1935  else
1936  {
1939  if (symbolDef)
1940  {
1941  symbolDef->Draw(cgoOut, false, 0UL, nullptr, 0, 0, nullptr, graphicsName ? graphicsName->getData() : nullptr);
1942  }
1943  }
1944 
1946 
1947  if (strBuf)
1948  {
1949  StdStrBuf sText;
1950  int alignment = ALeft;
1951  // If we are showing a scrollbar, we need to leave a bit of space for it so that it doesn't overlap the text.
1952  const int scrollbarXOffset = pScrollBar->IsVisible() ? pScrollBar->rcBounds.Wdt : 0;
1953  const int scrollbarScroll = pScrollBar->IsVisible() ? this->GetScrollY() : 0;
1954  const int actualDrawingWidth = outDrawWdt - scrollbarXOffset;
1955 
1956  // If we are set to NoCrop, the message will be split by string-defined line breaks only.
1957  int allowedTextWidth = actualDrawingWidth;
1958 
1960  allowedTextWidth = std::numeric_limits<int>::max();
1961  int32_t textHgt = ::GraphicsResource.FontRegular.BreakMessage(strBuf->getData(), allowedTextWidth, &sText, true);
1962  float textYOffset = static_cast<float>(-scrollbarScroll), textXOffset = 0.0f;
1964  textYOffset = float(outDrawHgt) / 2.0f - float(textHgt) / 2.0f;
1965  else if (style & C4ScriptGuiWindowStyleFlag::TextBottom)
1966  textYOffset = float(outDrawHgt) - float(textHgt);
1968  {
1969  int wdt, hgt;
1970  ::GraphicsResource.FontRegular.GetTextExtent(sText.getData(), wdt, hgt, true);
1971  textXOffset = float(actualDrawingWidth) / 2.0f;
1972  alignment = ACenter;
1973  }
1974  else if (style & C4ScriptGuiWindowStyleFlag::TextRight)
1975  {
1976  alignment = ARight;
1977  int wdt, hgt;
1978  ::GraphicsResource.FontRegular.GetTextExtent(sText.getData(), wdt, hgt, true);
1979  textXOffset = float(actualDrawingWidth);
1980  }
1981  pDraw->TextOut(sText.getData(), ::GraphicsResource.FontRegular, 1.0, cgo.Surface, outDrawX + textXOffset, outDrawY + textYOffset, 0xffffffff, alignment);
1982  }
1983 
1984 
1985  if (GraphicsSystem.ShowMenuInfo) // print helpful debug info
1986  {
1987  DWORD frameColor = C4RGB(100, 150, 100);
1988  if (currentMouseState & MouseState::Focus) frameColor = C4RGB(0, 255, 0);
1989 
1990  pDraw->DrawFrameDw(cgo.Surface, outDrawX, outDrawY, outDrawRight, outDrawBottom, frameColor);
1991  if (target || id)
1992  {
1993  StdStrBuf buf = FormatString("%s(%d)", target ? target->GetName() : "", id);
1994  pDraw->TextOut(buf.getData(), ::GraphicsResource.FontCaption, 1.0, cgo.Surface, cgo.X + outDrawRight, cgo.Y + outDrawBottom - ::GraphicsResource.FontCaption.GetLineHeight(), 0xffff00ff, ARight);
1995  }
1996  //StdStrBuf buf2 = FormatString("(%d, %d, %d, %d)", rcBounds.x, rcBounds.y, rcBounds.Wdt, rcBounds.Hgt);
1997  //pDraw->TextOut(buf2.getData(), ::GraphicsResource.FontCaption, 1.0, cgo.Surface, cgo.X + outDrawX + rcBounds.Wdt / 2, cgo.Y + outDrawY + +rcBounds.Hgt / 2, 0xff00ffff, ACenter);
1998  }
1999 
2000  DrawChildren(cgo, player, -1, currentClippingRect);
2001  return true;
2002 }
2003 
2004 bool C4ScriptGuiWindow::GetClippingRect(int32_t &left, int32_t &top, int32_t &right, int32_t &bottom)
2005 {
2006  const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
2007  if (IsRoot() || (style & C4ScriptGuiWindowStyleFlag::NoCrop))
2008  return false;
2009 
2010  // Other windows always clip their children.
2011  // This implicitly clips childrens' text to the parent size, too.
2012  left = rcBounds.x;
2013  top = rcBounds.y;
2014  right = rcBounds.Wdt + left;
2015  bottom = rcBounds.Hgt + top;
2016  return true;
2017 }
2018 
2020 {
2021  // set tag on all properties
2022  for (uint32_t i = 0; i < C4ScriptGuiWindowPropertyName::_lastProp; ++i)
2023  if (props[i].SwitchTag(tag))
2024  {
2025  // only if tag could have changed position etc.
2028  }
2029 
2030  // .. and children
2031  for (C4GUI::Element * element : *this)
2032  (static_cast<C4ScriptGuiWindow*>(element))->SetTag(tag);
2033 }
2034 
2036 {
2037  assert(::MouseControl.GetPlayer() != NO_OWNER);
2038 }
2039 
2040 void C4ScriptGuiWindow::OnMouseIn(int32_t player, int32_t parentOffsetX, int32_t parentOffsetY)
2041 {
2042  assert(!HasMouseFocus() && "custom menu window properly loaded incorrectly!");
2043  currentMouseState = MouseState::Focus;
2044 
2045  // no need to notify children, this is done in MouseInput
2046 
2047  // update tooltip info if applicable
2049  if (strBuf)
2050  {
2051  C4Viewport * viewport = ::Viewports.GetViewport(player);
2052  if (viewport)
2053  {
2054  const float guiZoom = viewport->GetGUIZoom();
2055  const float x = float(parentOffsetX + rcBounds.x) / guiZoom;
2056  const float y = float(parentOffsetY + rcBounds.y) / guiZoom;
2057  const float wdt = float(rcBounds.Wdt) / guiZoom;
2058  const float hgt = float(rcBounds.Hgt) / guiZoom;
2059  ::MouseControl.SetTooltipRectangle(C4Rect(x, y, wdt, hgt));
2060  ::MouseControl.SetTooltipText(*strBuf);
2061  }
2062  }
2063  // execute action
2065  C4ScriptGuiWindowAction *action = props[actionType].GetAction();
2066  if (!action) return;
2067  action->Execute(this, player, actionType);
2068 }
2069 
2071 {
2072  assert(::MouseControl.GetPlayer() != NO_OWNER);
2073 
2074 }
2076 {
2077  assert(HasMouseFocus() && "custom menu window probably loaded incorrectly!");
2079 
2080  // needs to notify children
2081  for (C4GUI::Element *iter : *this)
2082  {
2083  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(iter);
2084  if (child->HasMouseFocus())
2085  child->OnMouseOut(player);
2086  }
2087 
2088  // execute action
2090  C4ScriptGuiWindowAction *action = props[actionType].GetAction();
2091  if (!action) return;
2092  action->Execute(this, player, actionType);
2093 }
2094 
2095 bool C4ScriptGuiWindow::MouseInput(int32_t button, int32_t mouseX, int32_t mouseY, DWORD dwKeyParam)
2096 {
2097  // only called on root
2098  assert(IsRoot());
2099  // This is only called during a mouse move event, where the MouseControl's player is available.
2100  const int32_t &player = ::MouseControl.GetPlayer();
2101  assert(player != NO_OWNER);
2102  // Only allow one window to catch the mouse input.
2103  // Do not simply return, however, since other windows might need OnMouseOut().
2104  bool oneActionAlreadyExecuted = false;
2105  // non-multiple-windows have a higher priority
2106  // this is important since they are also drawn on top
2107  for (int withMultipleFlag = 0; withMultipleFlag <= 1; ++withMultipleFlag)
2108  {
2109  for (auto iter = rbegin(); iter != rend(); ++iter)
2110  {
2111  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(*iter);
2112 
2113  const int32_t &style = child->props[C4ScriptGuiWindowPropertyName::style].GetInt();
2114  if ((withMultipleFlag == 0) && (style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
2115  if ((withMultipleFlag == 1) && !(style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
2116 
2117  // Do the visibility check first. The child itself won't do it, because we are handling mouse in/out here, too.
2118  if (!child->IsVisibleTo(player)) continue;
2119 
2120  // we are root, we have to adjust the position for the "main" windows that are centered
2121  int32_t adjustedMouseX = 0, adjustedMouseY = mouseY;
2122  int32_t offsetX = 0, offsetY = 0;
2123  if (withMultipleFlag == 0)
2124  {
2125  offsetX = -rcBounds.x;
2126  offsetY = -rcBounds.y;
2127  }
2128 
2129  adjustedMouseX = mouseX + offsetX;
2130  adjustedMouseY = mouseY + offsetY;
2131 
2132  int32_t childLeft = child->rcBounds.x;
2133  int32_t childRight = child->rcBounds.x + child->rcBounds.Wdt;
2134  int32_t childTop = child->rcBounds.y;
2135  int32_t childBottom = child->rcBounds.y + child->rcBounds.Hgt;
2136 
2137  bool inArea = true;
2138  if ((adjustedMouseX < childLeft) || (adjustedMouseX > childRight)) inArea = false;
2139  else if ((adjustedMouseY < childTop) || (adjustedMouseY > childBottom)) inArea = false;
2140 
2141  if (!inArea) // notify child if it had mouse focus before
2142  {
2143  if (child->HasMouseFocus())
2144  child->OnMouseOut(player);
2145  continue;
2146  }
2147  // Don't break since some more OnMouseOut might be necessary
2148  if (oneActionAlreadyExecuted) continue;
2149 
2150 
2151  // keep the mouse coordinates relative to the child's bounds
2152  if (child->ProcessMouseInput(button, adjustedMouseX - childLeft, adjustedMouseY - childTop - iScrollY, dwKeyParam, childLeft - offsetX, childTop + iScrollY - offsetY))
2153  {
2154  oneActionAlreadyExecuted = true;
2155  }
2156  }
2157  }
2158 
2159  return oneActionAlreadyExecuted;
2160 }
2161 
2162 bool C4ScriptGuiWindow::ProcessMouseInput(int32_t button, int32_t mouseX, int32_t mouseY, DWORD dwKeyParam, int32_t parentOffsetX, int32_t parentOffsetY)
2163 {
2164  const int32_t &player = ::MouseControl.GetPlayer();
2165  assert(player != NO_OWNER);
2166 
2167  // completely ignore mouse if the appropriate flag is set
2168  const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
2170  return false;
2171 
2172  // we have mouse focus! Is this new?
2173  if (!HasMouseFocus())
2174  OnMouseIn(player, parentOffsetX, parentOffsetY);
2175 
2176  // Make sure the UI does not catch release events without matching key-down events.
2177  // Otherwise, you could e.g. open a menu on left-down and then the menu would block the left-up event, leading to issues.
2178  if (button == C4MC_Button_LeftUp)
2179  {
2180  // Do not catch up-events without prior down-events!
2181  if (!(currentMouseState & MouseState::MouseDown)) return false;
2182  }
2183 
2184  // do not simply break the loop since some OnMouseOut might go missing
2185  bool oneActionAlreadyExecuted = false;
2186 
2187  const int32_t scrollAdjustedMouseY = mouseY + iScrollY;
2188 
2189  // children actually have a higher priority
2190  bool overChild = false; // remember for later, catch all actions that are in theory over children, even if not reaction (if main window)
2191  // use reverse iterator since children with higher Priority appear later in the list
2192  for (auto iter = rbegin(); iter != rend(); ++iter)
2193  {
2194  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(*iter);
2195 
2196  // Do the visibility check first. The child itself won't do it, because we are handling mouse in/out here, too.
2197  if (!child->IsVisibleTo(player)) continue;
2198 
2199  const int32_t childLeft = child->rcBounds.x;
2200  const int32_t childRight = child->rcBounds.x + child->rcBounds.Wdt;
2201  const int32_t childTop = child->rcBounds.y;
2202  const int32_t childBottom = child->rcBounds.y + child->rcBounds.Hgt;
2203 
2204  bool inArea = true;
2205  if ((mouseX <= childLeft) || (mouseX > childRight)) inArea = false;
2206  else if ((scrollAdjustedMouseY <= childTop) || (scrollAdjustedMouseY > childBottom)) inArea = false;
2207 
2208  if (!inArea) // notify child if it had mouse focus before
2209  {
2210  if (child->HasMouseFocus())
2211  child->OnMouseOut(player);
2212  continue;
2213  }
2214 
2215  if (oneActionAlreadyExecuted) continue;
2216 
2217  overChild = true;
2218  // keep coordinates relative to children
2219  if (child->ProcessMouseInput(button, mouseX - childLeft, scrollAdjustedMouseY - childTop, dwKeyParam, parentOffsetX + rcBounds.x, parentOffsetY + rcBounds.y - iScrollY))
2220  {
2221  oneActionAlreadyExecuted = true;
2222  }
2223  }
2224 
2225  if (oneActionAlreadyExecuted) return true;
2226 
2227  //C4GUI::Element::MouseInput(rMouse, button, mouseX, mouseY, dwKeyParam);
2228 
2229  // remember button-down events. The action will only be executed on button-up
2230  // The sequence for double-clicks is LeftDown-LeftUp-LeftDouble-LeftUp, so treat double as down
2231  if (button == C4MC_Button_LeftDown || button == C4MC_Button_LeftDouble)
2232  currentMouseState |= MouseState::MouseDown;
2233  // trigger!
2234  if (button == C4MC_Button_LeftUp)
2235  {
2238  if (action)
2239  {
2241  return true;
2242  }
2243  }
2244 
2245  // for scroll-enabled windows, scroll contents with wheel
2246  if (pScrollBar->IsVisible() && (button == C4MC_Button_Wheel))
2247  {
2248  short delta = (short)(dwKeyParam >> 16);
2249  ScrollBy(-delta);
2250  //float fac = (lastDrawPosition.bottomMostChild - lastDrawPosition.topMostChild);
2251  //if (fac == 0.0f) fac = 1.0f;
2252  //scrollBar->ScrollBy(-float(delta) / fac);
2253  return true;
2254  }
2255 
2256  // forward to scroll-bar if in area
2257  if (pScrollBar->IsVisible())
2258  {
2259  if ((mouseX > pScrollBar->rcBounds.x && mouseX < pScrollBar->rcBounds.x + pScrollBar->rcBounds.Wdt)
2260  && (mouseY > pScrollBar->rcBounds.y && mouseY < pScrollBar->rcBounds.y + pScrollBar->rcBounds.Hgt))
2261  {
2262  C4GUI::CMouse mouse(mouseX, mouseY);
2263  if (::MouseControl.IsLeftDown()) mouse.LDown = true;
2264  pScrollBar->MouseInput(mouse, button, mouseX - pScrollBar->rcBounds.x, mouseY - pScrollBar->rcBounds.y, dwKeyParam);
2265  }
2266  }
2267 
2268 
2269  // if the user still clicked on a menu - even if it didn't do anything, catch it
2270  // but do that only on the top-level to not stop traversing other branches
2271  if (isMainWindow)
2272  return overChild;
2273  return false;
2274 }
2275 
2276 bool C4ScriptGuiWindow::ExecuteCommand(int32_t actionID, int32_t player, int32_t subwindowID, int32_t actionType, C4Object *target)
2277 {
2278  if (isMainWindow && subwindowID) // we are a main window! try a shortcut through the ID?
2279  {
2280  MenuDebugLogF("passing command... instance:%d, plr:%d, subwin:%d, type:%d [I am %d, MW]", actionID, player, subwindowID, actionType, id);
2281  MenuDebugLogF("stats:");
2282  MenuDebugLogF("active menus:\t%d", GetElementCount());
2283  MenuDebugLogF("children ID map:\t%d", childrenIDMap.size());
2284  // the reasoning for that shortcut is that I assume that usually windows with actions will also have an ID assigned
2285  // this obviously doesn't have to be the case, but I believe it's worth the try
2286  std::pair<std::multimap<int32_t, C4ScriptGuiWindow*>::iterator, std::multimap<int32_t, C4ScriptGuiWindow*>::iterator> range;
2287  range = childrenIDMap.equal_range(subwindowID);
2288 
2289  for (std::multimap<int32_t, C4ScriptGuiWindow*>::iterator iter = range.first; iter != range.second; ++iter)
2290  {
2291  if (iter->second->ExecuteCommand(actionID, player, subwindowID, actionType, target))
2292  {
2293  MenuDebugLogF("shortcutting command sucessful!");
2294  return true;
2295  }
2296  }
2297  // it is not possible that another window would match the criteria. Abort later after self-check
2298  MenuDebugLogF("shortcutting command failed.. no appropriate window");
2299  }
2300 
2301  // are we elligible?
2302  if ((id == subwindowID) && (this->target == target))
2303  {
2304  MenuDebugLogF("stats: (I am %d)", id);
2305  MenuDebugLogF("children:\t%d", GetElementCount());
2306  MenuDebugLogF("all actions:\t%d", props[actionType].GetAllActions().size());
2307  std::list<C4ScriptGuiWindowAction*> allActions = props[actionType].GetAllActions();
2308  for (auto action : allActions)
2309  {
2310  assert(action && "C4ScriptGuiWindowProperty::GetAllActions returned list with null-pointer");
2311 
2312  if (action->ExecuteCommand(actionID, this, player))
2313  {
2314  MenuDebugLogF("executing command sucessful!");
2315  return true;
2316  }
2317  }
2318 
2319  // note that we should not simply return false here
2320  // there is no guarantee that only one window with that target&ID exists
2321  }
2322 
2323  // not caught, forward to children!
2324  // abort if main window, though. See above
2325  if (isMainWindow && subwindowID)
2326  {
2327  MenuDebugLogF("executing command failed!");
2328  return false;
2329  }
2330 
2331  // otherwise, just pass to children..
2332  for (C4GUI::Element *element : *this)
2333  {
2334  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
2335  if (child->ExecuteCommand(actionID, player, subwindowID, actionType, target))
2336  {
2337  MenuDebugLogF("passing command sucessful! (I am %d - &p)", id, this->target);
2338  return true;
2339  }
2340  }
2341  return false;
2342 }
2343 
2344 bool C4ScriptGuiWindow::IsRoot()
2345 {
2346  return this == Game.ScriptGuiRoot.get();
2347 }
2348 
2350 {
2351  // Not visible at all?
2352  if (!IsVisible()) return false;
2353  // We have a player assigned and it's a different one?
2354  const int32_t &myPlayer = props[C4ScriptGuiWindowPropertyName::player].GetInt();
2355  if (myPlayer != ANY_OWNER && player != myPlayer) return false;
2356  // We have a target object which is invisible to the player?
2357  if (target && !target->IsVisible(player, false)) return false;
2358  // Default to visible!
2359  return true;
2360 }
const char * getData() const
Definition: StdBuf.h:442
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(...)
std::list< C4ScriptGuiWindowAction * > GetAllActions()
bool RestorePrimaryClipper()
Definition: C4Draw.cpp:210
void SetItem(int32_t iElemNr, const C4Value &Value)
const int32_t C4MC_Button_LeftDown
int GetLineHeight() const
Definition: C4FontLoader.h:125
float Y
Definition: C4Facet.h:118
float Zoom
Definition: C4Facet.h:165
C4String * getStr() const
Definition: C4Value.h:117
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:2340
bool IsVisible()
Definition: C4Gui.cpp:201
const char * GetCStr() const
Definition: C4StringTable.h:49
C4String * RegString(StdStrBuf String)
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:567
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:120
const int ARight
Definition: C4Surface.h:41
Iterator end()
Definition: C4PropList.h:206
Definition: C4Rect.h:27
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:174
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:88
virtual C4Object * GetObject()
Definition: C4PropList.cpp:663
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:681
bool IsVisibleTo(int32_t player)
int32_t Wdt
Definition: C4Rect.h:30
void DrawFrameDw(C4Surface *sfcDest, int x1, int y1, int x2, int y2, DWORD dwClr, float width=1.0f)
Definition: C4Draw.cpp:641
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:207
bool SwitchTag(C4String *tag)
const int32_t C4MC_Button_LeftUp
void MouseInput(CMouse &rMouse, int32_t iButton, int32_t iX, int32_t iY, DWORD dwKeyParam) override
int32_t GetPlayer()
int32_t y
Definition: C4Rect.h:30
void Draw(C4TargetFacet &cgo) override
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:41
virtual bool GetPropertyByS(const C4String *k, C4Value *pResult) const
Definition: C4PropList.cpp:753
int32_t iClientHeight
Definition: C4Gui.h:944
Iterator begin()
virtual const char * GetName() const
Definition: C4PropList.cpp:645
C4GraphicsSystem GraphicsSystem
Definition: C4Globals.cpp:51
virtual C4ValueArray * GetProperties() const
Definition: C4PropList.cpp:910
ReverseIterator rend()
Definition: C4Gui.h:827
void DrawBoxDw(C4Surface *sfcDest, int iX1, int iY1, int iX2, int iY2, DWORD dwClr)
Definition: C4Draw.cpp:846
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:599
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:98
const int NO_OWNER
Definition: C4Constants.h:138
void SetSize(int32_t inSize)
C4Draw * pDraw
Definition: C4Draw.cpp:42
bool fAutoHide
Definition: C4Gui.h:884
#define C4RGB(r, g, b)
Definition: StdColors.h:26
int32_t Status
Definition: C4PropList.h:168
C4ValueArray * getArray() const
Definition: C4Value.h:118
bool SetPrimaryClipper(int iX1, int iY1, int iX2, int iY2)
Definition: C4Draw.cpp:217
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:134
C4Control & Input
Definition: C4Game.h:82
void OnMouseIn(int32_t player, int32_t parentOffsetX, int32_t parentOffsetY)
int32_t getInt() const
Definition: C4Value.h:112
void RemoveElement(Element *pChild) override
int32_t x
Definition: C4Rect.h:30
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:30
C4ScriptGuiWindowAction * GetAction()
const C4Value ToC4Value(bool first=true)
float TargetX
Definition: C4Facet.h:165
C4ViewportList Viewports
Definition: C4Viewport.cpp:841
C4ScriptGuiWindow * AddChild()
static const float standardWidth
bool UpdateLayout(C4TargetFacet &cgo)
float Hgt
Definition: C4Facet.h:118
Iterator end()
Definition: C4Gui.h:825
int main(int argc, char *argv[])
const int ALeft
Definition: C4Surface.h:41
bool CreateFromPropList(C4PropList *proplist, bool resetStdTag=false, bool isUpdate=false, bool isLoading=false)
C4Def * getDef() const
Definition: C4Value.cpp:78
bool UpdateChildLayout(C4TargetFacet &cgo, float parentWidth, float parentHeight)
float GetGUIZoom() const
Definition: C4Viewport.h:47
void MouseLeave(C4GUI::CMouse &rMouse) override
void MouseEnter(C4GUI::CMouse &rMouse) override
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:948
int32_t Hgt
Definition: C4Rect.h:30
std::unique_ptr< C4ScriptGuiWindow > ScriptGuiRoot
Definition: C4Game.h:233
const C4Value & GetValue(uint32_t)
Definition: C4Value.cpp:243
C4Surface * Surface
Definition: C4Facet.h:117
void DecRef()
Definition: C4StringTable.h:28
float TargetY
Definition: C4Facet.h:165
#define PROPERTY_TUPLE(p, prop1, prop2)
uint32_t DWORD
bool LogF(const char *strMessage,...)
Definition: C4Log.cpp:250
const C4Value ToC4Value()
void Copy()
Definition: StdBuf.h:467
float Wdt
Definition: C4Facet.h:118
void Set(const C4Value &value, C4String *tag)
#define s
void DrawElement(C4TargetFacet &cgo) override
float X
Definition: C4Facet.h:118
C4ScriptGuiWindow * GetChildByID(int32_t child)
C4Object * getObj() const
Definition: C4Value.cpp:68
void ClearPointers(C4Object *pObj)
bool IsVisible(int32_t iForPlr, bool fAsOverlay) const
Definition: C4Object.cpp:4051
bool StorePrimaryClipper()
Definition: C4Draw.cpp:203
bool ExecuteCommand(int32_t actionID, C4ScriptGuiWindow *parent, int32_t player)
static C4PropList * New(C4PropList *prototype=nullptr)
Definition: C4PropList.cpp:64
StdStrBuf FormatString(const char *szFmt,...)
Definition: StdBuf.cpp:270
C4Value Call(C4PropertyName k, C4AulParSet *pPars=nullptr, bool fPassErrors=false)
Definition: C4PropList.h:110
C4PropList * getPropList() const
Definition: C4Value.h:116