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::type 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::type relative, C4ScriptGuiWindowPropertyName::type absolute, C4String *tag)
773 {
774  // the value might be a tagged proplist again
775  if (property.GetType() == C4V_Type::C4V_PropList)
776  {
777  C4PropList *proplist = property.getPropList();
778  for (C4PropList::Iterator iter = proplist->begin(); iter != proplist->end(); ++iter)
779  {
780  SetPositionStringProperties(iter->Value, relative, absolute, iter->Key);
781  }
782  return;
783  }
784  // safety
785  if (property.GetType() != C4V_Type::C4V_String) {
786  if(property.GetType() != C4V_Type::C4V_Nil)
787  LogF("Warning: Got %s instead of expected menu format string.", property.GetTypeName());
788  return;
789  }
790 
791  float relativeValue = 0.0;
792  float absoluteValue = 0.0;
793 
794  std::locale c_locale("C");
795  std::istringstream reader(std::string(property.getStr()->GetCStr()));
796  reader.imbue(c_locale);
797  if(!reader.good()) return;
798 
799  while (!reader.eof())
800  {
801  reader >> std::ws; // eat white space
802 
803  // look for next float
804  float value;
805  // here comes the fun.
806  // strtod is locale dependent
807  // istringstream will try to parse scientific notation, so things like 3em will be tried to be parsed as 3e<exponent> and consequently fail
808  // thus, per stackoverflow recommendation, parse the float into a separate string and then let that be parsed
809  std::stringstream floatss;
810  floatss.imbue(c_locale);
811  if(reader.peek() == '+' || reader.peek() == '-') floatss.put(reader.get());
812  reader >> std::ws;
813  while(std::isdigit(reader.peek()) || reader.peek() == '.') floatss.put(reader.get());
814  floatss >> value;
815  reader >> std::ws;
816 
817  if (reader.peek() == '%')
818  {
819  relativeValue += value;
820  reader.get();
821  }
822  else if (reader.get() == 'e' && reader.get() == 'm')
823  {
824  absoluteValue += value;
825  }
826  else // error, abort! (readere is not in a clean state anyway)
827  {
828  LogF(R"(Warning: Could not parse menu format string "%s"!)", property.getStr()->GetCStr());
829  return;
830  }
831 
832  reader.peek(); // get eof bit to be set
833  }
834  props[relative].SetFloat(relativeValue / 100.0f, tag);
835  props[absolute].SetFloat(absoluteValue, tag);
836 }
837 
838 // for saving
839 C4Value C4ScriptGuiWindow::PositionToC4Value(C4ScriptGuiWindowPropertyName::type relativeName, C4ScriptGuiWindowPropertyName::type absoluteName)
840 {
841  // Go through all tags of the position attributes and save.
842  // Note that the tags for both the relative and the absolute attribute are always the same.
843  C4ScriptGuiWindowProperty &relative = props[relativeName];
844  C4ScriptGuiWindowProperty &absolute = props[absoluteName];
845 
846  C4PropList *proplist = nullptr;
847  const bool onlyStdTag = relative.taggedProperties.size() == 1;
848  for (auto & taggedProperty : relative.taggedProperties)
849  {
850  C4String *tag = taggedProperty.first;
851  StdStrBuf buf;
852  buf.Format("%f%%%+fem", 100.0f * taggedProperty.second.f, absolute.taggedProperties[tag].f);
853  C4String *propString = Strings.RegString(buf);
854 
855  if (onlyStdTag)
856  return C4Value(propString);
857  else
858  {
859  if (proplist == nullptr)
860  proplist = C4PropList::New();
861  proplist->SetPropertyByS(tag, C4Value(propString));
862  }
863  }
864  return C4Value(proplist);
865 }
866 
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 
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  using namespace C4ScriptGuiWindowPropertyName;
1406  const float childLeftMargin = child->CalculateRelativeSize(width, leftMargin, relLeftMargin);
1407  const float childTopMargin = child->CalculateRelativeSize(height, topMargin, relTopMargin);
1408  const float childRightMargin = child->CalculateRelativeSize(width, rightMargin, relRightMargin);
1409  const float childBottomMargin = child->CalculateRelativeSize(height, bottomMargin, relBottomMargin);
1410 
1411  const float childWdtF = float(child->rcBounds.Wdt) + childLeftMargin + childRightMargin;
1412  const float childHgtF = float(child->rcBounds.Hgt) + childTopMargin + childBottomMargin;
1413 
1414  auto doLineBreak = [&]()
1415  {
1416  currentX = borderX;
1417  currentY += maxChildHeight + borderY;
1418  maxChildHeight = 0;
1419  };
1420 
1421  // do all the rounding after the calculations
1422  const auto childWdt = (int32_t)(childWdtF + 0.5f);
1423  const auto childHgt = (int32_t)(childHgtF + 0.5f);
1424 
1425  // Check if the child even fits in the remainder of the row
1426  const bool fitsInRow = (width - currentX) >= childWdt;
1427  if (!fitsInRow) doLineBreak();
1428 
1429  // remember the highest child to make sure rows don't overlap
1430  if (!maxChildHeight || (childHgt > maxChildHeight))
1431  {
1432  maxChildHeight = childHgt;
1433  lowestChildRelY = currentY + childHgt;
1434  }
1435  child->rcBounds.x = currentX + static_cast<int32_t>(childLeftMargin);
1436  child->rcBounds.y = currentY + static_cast<int32_t>(childTopMargin);
1437 
1438  currentX += childWdt + borderX;
1439  }
1440 
1441  // do we need a scroll bar?
1442  EnableScrollBar(lowestChildRelY > height, lowestChildRelY);
1443 }
1444 
1445 // Similar to the grid layout but tries to fill spaces more thoroughly.
1446 // It's slower and might reorder items.
1448 {
1449  const int32_t &width = rcBounds.Wdt;
1450  const int32_t &height = rcBounds.Hgt;
1451  const int32_t borderX(0), borderY(0);
1452  int32_t lowestChildRelY = 0;
1453 
1454  std::list<C4ScriptGuiWindow*> alreadyPlacedChildren;
1455 
1456  for (C4GUI::Element * element : *this)
1457  {
1458  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1459  // calculate the space the child needs, correctly respecting the margins
1460  using namespace C4ScriptGuiWindowPropertyName;
1461  const float childLeftMargin = child->CalculateRelativeSize(width, leftMargin, relLeftMargin);
1462  const float childTopMargin = child->CalculateRelativeSize(height, topMargin, relTopMargin);
1463  const float childRightMargin = child->CalculateRelativeSize(width, rightMargin, relRightMargin);
1464  const float childBottomMargin = child->CalculateRelativeSize(height, bottomMargin, relBottomMargin);
1465 
1466  const float childWdtF = float(child->rcBounds.Wdt) + childLeftMargin + childRightMargin;
1467  const float childHgtF = float(child->rcBounds.Hgt) + childTopMargin + childBottomMargin;
1468 
1469  // do all the rounding after the calculations
1470  const auto childWdt = (int32_t)(childWdtF + 0.5f);
1471  const auto childHgt = (int32_t)(childHgtF + 0.5f);
1472 
1473  // Look for a free spot.
1474  int32_t currentX = borderX;
1475  int32_t currentY = borderY;
1476 
1477  bool hadOverlap = false;
1478  int overlapRepeats = 0;
1479  do
1480  {
1481  auto overlapsWithOther = [&currentX, &currentY, &childWdt, &childHgt](C4ScriptGuiWindow *other)
1482  {
1483  if (currentX + childWdt <= other->rcBounds.x) return false;
1484  if (currentY + childHgt <= other->rcBounds.y) return false;
1485  if (currentX >= other->rcBounds.GetRight()) return false;
1486  if (currentY >= other->rcBounds.GetBottom()) return false;
1487  return true;
1488  };
1489 
1490  int32_t currentMinY = 0;
1491  hadOverlap = false;
1492  for (auto &other : alreadyPlacedChildren)
1493  {
1494  // Check if the other element is not yet above the new child.
1495  if ((other->rcBounds.GetBottom() > currentY) && other->rcBounds.Hgt > 0)
1496  {
1497  if (currentMinY == 0 || (other->rcBounds.GetBottom() < currentMinY))
1498  currentMinY = other->rcBounds.GetBottom();
1499  }
1500  // If overlapping, we must advance.
1501  if (overlapsWithOther(other))
1502  {
1503  hadOverlap = true;
1504  currentX = other->rcBounds.GetRight();
1505  // Break line if the element doesn't fit anymore.
1506  if (currentX + childWdt > width)
1507  {
1508  currentX = borderX;
1509  // Start forcing change once we start repeating the check. Otherwise, there might
1510  // be a composition of children that lead to infinite loop. The worst-case number
1511  // of sensible checks might O(N^2) be with a really unfortunate children list.
1512  const int32_t forcedMinimalChange = (overlapRepeats > alreadyPlacedChildren.size()) ? 1 : 0;
1513  currentY = std::max(currentY + forcedMinimalChange, currentMinY);
1514  }
1515  }
1516  }
1517  overlapRepeats += 1;
1518  } while (hadOverlap);
1519 
1520  alreadyPlacedChildren.push_back(child);
1521 
1522  lowestChildRelY = std::max(lowestChildRelY, currentY + childHgt);
1523  child->rcBounds.x = currentX + static_cast<int32_t>(childLeftMargin);
1524  child->rcBounds.y = currentY + static_cast<int32_t>(childTopMargin);
1525  }
1526 
1527  // do we need a scroll bar?
1528  EnableScrollBar(lowestChildRelY > height, lowestChildRelY);
1529 }
1530 
1532 {
1533  const int32_t borderY(0);
1534  int32_t currentY = borderY;
1535 
1536  for (C4GUI::Element * element : *this)
1537  {
1538  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1539 
1540  // Do the calculations in floats first to not lose accuracy.
1541  // Take the height of the child and then add the margins.
1542  using namespace C4ScriptGuiWindowPropertyName;
1543  const float childTopMargin = child->CalculateRelativeSize(rcBounds.Hgt, topMargin, relTopMargin);
1544  const float childBottomMargin = child->CalculateRelativeSize(rcBounds.Hgt, bottomMargin, relBottomMargin);
1545 
1546  const float childHgtF = float(child->rcBounds.Hgt) + childTopMargin + childBottomMargin;
1547  const int32_t childHgt = (int32_t)(childHgtF + 0.5f);
1548 
1549  child->rcBounds.y = currentY + childTopMargin;
1550  currentY += childHgt + borderY;
1551  }
1552 
1553  // do we need a scroll bar?
1554  EnableScrollBar(currentY > rcBounds.Hgt, currentY);
1555 }
1556 
1557 bool C4ScriptGuiWindow::DrawChildren(C4TargetFacet &cgo, int32_t player, int32_t withMultipleFlag, C4Rect *currentClippingRect)
1558 {
1559  // remember old target rectangle and adjust
1560  float oldTargetX = cgo.TargetX;
1561  float oldTargetY = cgo.TargetY;
1562  C4Rect myClippingRect;
1563  if (IsRoot())
1564  {
1565  cgo.TargetX = 0;
1566  cgo.TargetY = 0;
1568  // default: full screen clipper
1569  myClippingRect = C4Rect(0, 0, cgo.Wdt * cgo.Zoom, cgo.Hgt * cgo.Zoom);
1570  currentClippingRect = &myClippingRect;
1571  }
1572 
1573  // if ANY PARENT has scroll bar, then adjust clipper
1574  int32_t clipX1(0), clipX2(0), clipY1(0), clipY2(0);
1575  bool clipping = GetClippingRect(clipX1, clipY1, clipX2, clipY2);
1576 
1577  const int32_t targetClipX1 = cgo.X + cgo.TargetX + clipX1;
1578  const int32_t targetClipY1 = cgo.Y + cgo.TargetY + clipY1;
1579  const int32_t targetClipX2 = cgo.X + cgo.TargetX + clipX2;
1580  const int32_t targetClipY2 = cgo.Y + cgo.TargetY + clipY2;
1581 
1582  if (clipping)
1583  {
1584  // Take either the parent rectangle or restrict it additionally by the child's geometry.
1585  myClippingRect = C4Rect(
1586  std::max(currentClippingRect->x, targetClipX1),
1587  std::max(currentClippingRect->y, targetClipY1),
1588  std::min(currentClippingRect->Wdt, targetClipX2),
1589  std::min(currentClippingRect->Hgt, targetClipY2));
1590  currentClippingRect = &myClippingRect;
1591  }
1592 
1593  if (withMultipleFlag != 1)
1594  {
1595  cgo.TargetX += rcBounds.x;
1596  cgo.TargetY += rcBounds.y - iScrollY;
1597  }
1598  else
1599  {
1600  assert(IsRoot());
1601  assert(withMultipleFlag == 1);
1602  }
1603 
1604 
1605  // note that withMultipleFlag only plays a roll for the root-menu
1606  bool oneDrawn = false; // was at least one child drawn?
1607  //for (auto iter = rbegin(); iter != rend(); ++iter)
1608  for (auto element : *this)
1609  {
1610  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1611 
1612  if (withMultipleFlag != -1)
1613  {
1614  const int32_t &style = child->props[C4ScriptGuiWindowPropertyName::style].GetInt();
1615  if ((withMultipleFlag == 0) && (style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
1616  if ((withMultipleFlag == 1) && !(style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
1617  }
1618 
1619  pDraw->SetPrimaryClipper(currentClippingRect->x, currentClippingRect->y, currentClippingRect->Wdt, currentClippingRect->Hgt);
1620 
1621  if (child->Draw(cgo, player, currentClippingRect))
1622  oneDrawn = true;
1623  // draw only one window when drawing non-Multiple windows
1624  if (oneDrawn && (withMultipleFlag == 0)) break;
1625  }
1626 
1627  // Scrolling obviously does not affect the scroll bar.
1628  cgo.TargetY += iScrollY;
1629  // The scroll bar does not correct for the cgo offset (i.e. the upper board).
1630  cgo.TargetX += cgo.X;
1631  cgo.TargetY += cgo.Y;
1632 
1633  if (pScrollBar->IsVisible())
1634  pScrollBar->DrawElement(cgo);
1635 
1636  if (IsRoot())
1637  {
1639  }
1640 
1641  // restore target rectangle
1642  cgo.TargetX = oldTargetX;
1643  cgo.TargetY = oldTargetY;
1644  return oneDrawn;
1645 }
1646 
1648 {
1649  // directly requested on the root window?
1650  // That usually comes from another part of the engine (f.e. C4Viewport::RecalculateViewports) or from a multiple-window child
1651  if (!GetParent())
1652  {
1653  mainWindowNeedsLayoutUpdate = true;
1654  return;
1655  }
1656 
1657  // are we a direct child of the root?
1658  if (isMainWindow)
1659  {
1660  const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
1661 
1662  if (!(style & C4ScriptGuiWindowStyleFlag::Multiple)) // are we a simple centered window?
1663  {
1664  mainWindowNeedsLayoutUpdate = true;
1665  return;
1666  }
1667  else // we are one of the multiple windows.. the root better do a full refresh
1668  {}
1669  }
1670  // propagate to parent window
1671  static_cast<C4ScriptGuiWindow*>(GetParent())->RequestLayoutUpdate();
1672 }
1673 
1674 bool C4ScriptGuiWindow::UpdateChildLayout(C4TargetFacet &cgo, float parentWidth, float parentHeight)
1675 {
1676  for (Element * element : *this)
1677  {
1678  C4ScriptGuiWindow *window = static_cast<C4ScriptGuiWindow*>(element);
1679  window->UpdateLayout(cgo, parentWidth, parentHeight);
1680  }
1681  return true;
1682 }
1683 
1685 {
1686  assert(IsRoot()); // we are root
1687 
1688  // assume I am the root and use the a special rectangle in the viewport for drawing
1689  const float fullWidth = cgo.Wdt * cgo.Zoom - cgo.X;
1690  const float fullHeight = cgo.Hgt * cgo.Zoom - cgo.Y;
1691 
1692  // golden ratio defined default size!
1693  const float &targetWidthEm = C4ScriptGuiWindow::standardWidth;
1694  const float &targetHeightEm = C4ScriptGuiWindow::standardHeight;
1695 
1696  // adjust by viewport size
1697  const float minMarginPx = 50.0f;
1698  const float targetWidthPx = std::min(Em2Pix(targetWidthEm), fullWidth - 2.0f * minMarginPx);
1699  const float targetHeightPx = std::min(Em2Pix(targetHeightEm), fullHeight - 2.0f * minMarginPx);
1700 
1701  // calculate margins to center the window
1702  const float marginLeftRight = (fullWidth - targetWidthPx) / 2.0f;
1703  const float marginTopBottom = (fullHeight- targetHeightPx) / 2.0f;
1704 
1705  // we can only position the window by adjusting left/right/top/bottom
1706  const float &left = marginLeftRight;
1707  const float right = -marginLeftRight;
1708  const float &top = marginTopBottom;
1709  const float bottom = -marginTopBottom;
1710 
1711  // actual size, calculated from borders
1712  const float wdt = fullWidth - left + right;
1713  const float hgt = fullHeight - top + bottom;
1714 
1715  const bool needUpdate = mainWindowNeedsLayoutUpdate || (rcBounds.Wdt != int32_t(wdt)) || (rcBounds.Hgt != int32_t(hgt));
1716 
1717  if (needUpdate)
1718  {
1719  mainWindowNeedsLayoutUpdate = false;
1720 
1721  // these are the coordinates for the centered non-multiple windows
1722  rcBounds.x = static_cast<int>(left);
1723  rcBounds.y = static_cast<int>(top);
1724  rcBounds.Wdt = wdt;
1725  rcBounds.Hgt = hgt;
1726 
1727  // first update all multiple windows (that can cover the whole screen)
1728  for (Element * element : *this)
1729  {
1730  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1731  const int32_t &style = child->props[C4ScriptGuiWindowPropertyName::style].GetInt();
1732  if (!(style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
1733  child->UpdateLayout(cgo, fullWidth, fullHeight);
1734  }
1735  // then update all "main" windows in the center of the screen
1736  // todo: adjust the size of the main window based on the border-windows drawn before
1737  for (Element * element : *this)
1738  {
1739  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1740  const int32_t &style = child->props[C4ScriptGuiWindowPropertyName::style].GetInt();
1741  if ((style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
1742  child->UpdateLayout(cgo, wdt, hgt);
1743  }
1744 
1745  pScrollBar->SetVisibility(false);
1746  }
1747  return true;
1748 }
1749 
1750 bool C4ScriptGuiWindow::UpdateLayout(C4TargetFacet &cgo, float parentWidth, float parentHeight)
1751 {
1752  // fetch style
1753  const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
1754  // fetch current position as shortcut for overview
1755  const float &left = props[C4ScriptGuiWindowPropertyName::left].GetFloat();
1756  const float &right = props[C4ScriptGuiWindowPropertyName::right].GetFloat();
1757  const float &top = props[C4ScriptGuiWindowPropertyName::top].GetFloat();
1758  const float &bottom = props[C4ScriptGuiWindowPropertyName::bottom].GetFloat();
1759 
1762  const float &relTop = props[C4ScriptGuiWindowPropertyName::relTop].GetFloat();
1764 
1765  // same for margins
1770 
1775 
1776  // calculate drawing rectangle
1777  float leftDrawX = relLeft * parentWidth + Em2Pix(left) + (Em2Pix(leftMargin) + relLeftMargin * parentWidth);
1778  float rightDrawX = relRight * parentWidth + Em2Pix(right) - (Em2Pix(rightMargin) + relRightMargin * parentWidth);
1779  float topDrawY = relTop * parentHeight + Em2Pix(top) + (Em2Pix(topMargin) + relTopMargin * parentHeight);
1780  float bottomDrawY = relBottom * parentHeight + Em2Pix(bottom) - (Em2Pix(bottomMargin) + relBottomMargin * parentHeight);
1781  float width = rightDrawX - leftDrawX;
1782  float height = bottomDrawY - topDrawY;
1783 
1784  rcBounds.x = leftDrawX;
1785  rcBounds.y = topDrawY;
1786  rcBounds.Wdt = width;
1787  rcBounds.Hgt = height;
1788 
1789  // If this window contains text, we auto-fit to the text height;
1790  // but we only break text when the window /would/ crop it otherwise.
1792  int minRequiredTextHeight = 0;
1793  if (strBuf && !(style & C4ScriptGuiWindowStyleFlag::NoCrop))
1794  {
1795  StdStrBuf sText;
1796  const int32_t rawTextHeight = ::GraphicsResource.FontRegular.BreakMessage(strBuf->getData(), rcBounds.Wdt, &sText, true);
1797  // enable auto scroll
1798  if (rawTextHeight - 1 > rcBounds.Hgt)
1799  {
1800  // If we need to scroll, we will also have to add a scrollbar that takes up some width.
1801  // Recalculate the actual height, taking into account the scrollbar.
1802  // This is not done in the calculation earlier, because then e.g. a 2x1em field could not contain two letters
1803  // but would immediately add a linebreak.
1804  // In the case that this window auto-resizes (FitChildren), the small additional margin to the bottom should not matter much.
1805  const int32_t actualTextHeight = ::GraphicsResource.FontRegular.BreakMessage(strBuf->getData(), rcBounds.Wdt - pScrollBar->rcBounds.Wdt, &sText, true);
1806  minRequiredTextHeight = actualTextHeight;
1807  }
1808  else
1809  {
1810  // Otherwise, still set the minimum size to the text height (without scrollbar).
1811  // This is necessary so that e.g. Style::FitChildren works properly with pure text windows.
1812  minRequiredTextHeight = rawTextHeight;
1813  }
1814  }
1815 
1816  UpdateChildLayout(cgo, width, height);
1817 
1818  // update scroll bar
1819  // C4GUI::ScrollWindow::UpdateOwnPos();
1820 
1821  // special layout selected?
1823  UpdateLayoutGrid();
1828 
1829  // check if we need a scroll-bar
1830  int32_t topMostChild = 0;
1831  int32_t bottomMostChild = minRequiredTextHeight;
1832  for (Element * element : *this)
1833  {
1834  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
1835  const int32_t &childTop = child->rcBounds.y;
1836  const int32_t childBottom = childTop + child->rcBounds.Hgt;
1837  if (childTop < topMostChild) topMostChild = childTop;
1838  if (childBottom > bottomMostChild) bottomMostChild = childBottom;
1839  }
1840  // do we need to adjust our size to fit the child windows?
1841  if (style & C4ScriptGuiWindowStyleFlag::FitChildren)
1842  {
1843  rcBounds.Hgt = bottomMostChild;
1844  }
1845  // update scroll rectangle:
1846  // (subtract one against rounding errors)
1847  bottomMostChild = std::max(bottomMostChild, rcBounds.Hgt);
1848  iClientHeight = bottomMostChild - topMostChild - 1;
1850 
1853  pScrollBar->rcBounds.y = 0;
1855  pScrollBar->Update();
1856 
1857  // never show scrollbar on non-cropping windows
1858  if ((style & C4ScriptGuiWindowStyleFlag::NoCrop) || !C4GUI::ScrollWindow::IsScrollingNecessary())
1859  pScrollBar->SetVisibility(false);
1860  // The "dirty" flag is unset here. Note that it's only used for non "multiple"-style windows after startup.
1861  // The "multiple"-style windows are updated together when the root window does a full refresh.
1862  mainWindowNeedsLayoutUpdate = false;
1863  return true;
1864 }
1865 
1867 {
1868  assert(IsRoot()); // we are root
1869  if (!IsVisible()) return false;
1870  // if the viewport shows an upper-board, apply an offset to everything
1871  const int oldTargetX = cgo.TargetX;
1872  const int oldTargetY = cgo.TargetY;
1873  cgo.TargetX += cgo.X;
1874  cgo.TargetY += cgo.Y;
1875  // this will check whether the viewport resized and we need an update
1876  UpdateLayout(cgo);
1877  // step one: draw all multiple-tagged windows
1878  DrawChildren(cgo, player, 1);
1879  // TODO: adjust rectangle for main menu if multiple windows exist
1880  // step two: draw one "main" menu
1881  DrawChildren(cgo, player, 0);
1882  // ..and restore the offset
1883  cgo.TargetX = oldTargetX;
1884  cgo.TargetY = oldTargetY;
1885  return true;
1886 }
1887 
1888 bool C4ScriptGuiWindow::Draw(C4TargetFacet &cgo, int32_t player, C4Rect *currentClippingRect)
1889 {
1890  assert(!IsRoot()); // not root, root needs to receive DrawAll
1891 
1892  // message hidden?
1893  if (!IsVisibleTo(player)) return false;
1894 
1895  const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
1896 
1897  if (mainWindowNeedsLayoutUpdate)
1898  {
1899  assert(GetParent() && (static_cast<C4ScriptGuiWindow*>(GetParent())->IsRoot()));
1900  assert(isMainWindow);
1901  assert(!(style & C4ScriptGuiWindowStyleFlag::Multiple) && "\"Multiple\"-style window not updated by a full refresh of the root window.");
1902  C4ScriptGuiWindow * parent = static_cast<C4ScriptGuiWindow*>(GetParent());
1903  UpdateLayout(cgo, parent->rcBounds.Wdt, parent->rcBounds.Hgt);
1904  assert(!mainWindowNeedsLayoutUpdate);
1905  }
1906 
1907  const int32_t outDrawX = cgo.X + cgo.TargetX + rcBounds.x;
1908  const int32_t outDrawY = cgo.Y + cgo.TargetY + rcBounds.y;
1909  const int32_t outDrawWdt = rcBounds.Wdt;
1910  const int32_t outDrawHgt = rcBounds.Hgt;
1911  const int32_t outDrawRight = outDrawX + rcBounds.Wdt;
1912  const int32_t outDrawBottom = outDrawY + rcBounds.Hgt;
1913  // draw various properties
1914  C4Facet cgoOut(cgo.Surface, outDrawX, outDrawY, outDrawWdt, outDrawHgt);
1915 
1917  if (backgroundColor)
1918  pDraw->DrawBoxDw(cgo.Surface, outDrawX, outDrawY, outDrawRight - 1.0f, outDrawBottom - 1.0f, backgroundColor);
1919 
1921 
1922  if (frameDecoration)
1923  {
1924  // the frame decoration will adjust for cgo.TargetX/Y itself
1925  C4Rect rect(
1926  outDrawX - frameDecoration->iBorderLeft - cgo.TargetX,
1927  outDrawY - frameDecoration->iBorderTop - cgo.TargetY,
1928  outDrawWdt + frameDecoration->iBorderRight + frameDecoration->iBorderLeft,
1929  outDrawHgt + frameDecoration->iBorderBottom + frameDecoration->iBorderTop);
1930  frameDecoration->Draw(cgo, rect);
1931  }
1932 
1934  if (symbolObject)
1935  {
1936  symbolObject->DrawPicture(cgoOut, false, nullptr);
1937  }
1938  else
1939  {
1942  if (symbolDef)
1943  {
1944  symbolDef->Draw(cgoOut, false, 0UL, nullptr, 0, 0, nullptr, graphicsName ? graphicsName->getData() : nullptr);
1945  }
1946  }
1947 
1949 
1950  if (strBuf)
1951  {
1952  StdStrBuf sText;
1953  int alignment = ALeft;
1954  // If we are showing a scrollbar, we need to leave a bit of space for it so that it doesn't overlap the text.
1955  const int scrollbarXOffset = pScrollBar->IsVisible() ? pScrollBar->rcBounds.Wdt : 0;
1956  const int scrollbarScroll = pScrollBar->IsVisible() ? this->GetScrollY() : 0;
1957  const int actualDrawingWidth = outDrawWdt - scrollbarXOffset;
1958 
1959  // If we are set to NoCrop, the message will be split by string-defined line breaks only.
1960  int allowedTextWidth = actualDrawingWidth;
1961 
1963  allowedTextWidth = std::numeric_limits<int>::max();
1964  int32_t textHgt = ::GraphicsResource.FontRegular.BreakMessage(strBuf->getData(), allowedTextWidth, &sText, true);
1965  float textYOffset = static_cast<float>(-scrollbarScroll), textXOffset = 0.0f;
1967  textYOffset = float(outDrawHgt) / 2.0f - float(textHgt) / 2.0f;
1968  else if (style & C4ScriptGuiWindowStyleFlag::TextBottom)
1969  textYOffset = float(outDrawHgt) - float(textHgt);
1971  {
1972  int wdt, hgt;
1973  ::GraphicsResource.FontRegular.GetTextExtent(sText.getData(), wdt, hgt, true);
1974  textXOffset = float(actualDrawingWidth) / 2.0f;
1975  alignment = ACenter;
1976  }
1977  else if (style & C4ScriptGuiWindowStyleFlag::TextRight)
1978  {
1979  alignment = ARight;
1980  int wdt, hgt;
1981  ::GraphicsResource.FontRegular.GetTextExtent(sText.getData(), wdt, hgt, true);
1982  textXOffset = float(actualDrawingWidth);
1983  }
1984  pDraw->TextOut(sText.getData(), ::GraphicsResource.FontRegular, 1.0, cgo.Surface, outDrawX + textXOffset, outDrawY + textYOffset, 0xffffffff, alignment);
1985  }
1986 
1987 
1988  if (GraphicsSystem.ShowMenuInfo) // print helpful debug info
1989  {
1990  DWORD frameColor = C4RGB(100, 150, 100);
1991  if (currentMouseState & MouseState::Focus) frameColor = C4RGB(0, 255, 0);
1992 
1993  pDraw->DrawFrameDw(cgo.Surface, outDrawX, outDrawY, outDrawRight, outDrawBottom, frameColor);
1994  if (target || id)
1995  {
1996  StdStrBuf buf = FormatString("%s(%d)", target ? target->GetName() : "", id);
1997  pDraw->TextOut(buf.getData(), ::GraphicsResource.FontCaption, 1.0, cgo.Surface, cgo.X + outDrawRight, cgo.Y + outDrawBottom - ::GraphicsResource.FontCaption.GetLineHeight(), 0xffff00ff, ARight);
1998  }
1999  //StdStrBuf buf2 = FormatString("(%d, %d, %d, %d)", rcBounds.x, rcBounds.y, rcBounds.Wdt, rcBounds.Hgt);
2000  //pDraw->TextOut(buf2.getData(), ::GraphicsResource.FontCaption, 1.0, cgo.Surface, cgo.X + outDrawX + rcBounds.Wdt / 2, cgo.Y + outDrawY + +rcBounds.Hgt / 2, 0xff00ffff, ACenter);
2001  }
2002 
2003  DrawChildren(cgo, player, -1, currentClippingRect);
2004  return true;
2005 }
2006 
2007 bool C4ScriptGuiWindow::GetClippingRect(int32_t &left, int32_t &top, int32_t &right, int32_t &bottom)
2008 {
2009  const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
2010  if (IsRoot() || (style & C4ScriptGuiWindowStyleFlag::NoCrop))
2011  return false;
2012 
2013  // Other windows always clip their children.
2014  // This implicitly clips childrens' text to the parent size, too.
2015  left = rcBounds.x;
2016  top = rcBounds.y;
2017  right = rcBounds.Wdt + left;
2018  bottom = rcBounds.Hgt + top;
2019  return true;
2020 }
2021 
2023 {
2024  // set tag on all properties
2025  for (uint32_t i = 0; i < C4ScriptGuiWindowPropertyName::_lastProp; ++i)
2026  if (props[i].SwitchTag(tag))
2027  {
2028  // only if tag could have changed position etc.
2031  }
2032 
2033  // .. and children
2034  for (C4GUI::Element * element : *this)
2035  (static_cast<C4ScriptGuiWindow*>(element))->SetTag(tag);
2036 }
2037 
2039 {
2040  assert(::MouseControl.GetPlayer() != NO_OWNER);
2041 }
2042 
2043 void C4ScriptGuiWindow::OnMouseIn(int32_t player, int32_t parentOffsetX, int32_t parentOffsetY)
2044 {
2045  assert(!HasMouseFocus() && "custom menu window properly loaded incorrectly!");
2046  currentMouseState = MouseState::Focus;
2047 
2048  // no need to notify children, this is done in MouseInput
2049 
2050  // update tooltip info if applicable
2052  if (strBuf)
2053  {
2054  C4Viewport * viewport = ::Viewports.GetViewport(player);
2055  if (viewport)
2056  {
2057  const float guiZoom = viewport->GetGUIZoom();
2058  const float x = float(parentOffsetX + rcBounds.x) / guiZoom;
2059  const float y = float(parentOffsetY + rcBounds.y) / guiZoom;
2060  const float wdt = float(rcBounds.Wdt) / guiZoom;
2061  const float hgt = float(rcBounds.Hgt) / guiZoom;
2062  ::MouseControl.SetTooltipRectangle(C4Rect(x, y, wdt, hgt));
2063  ::MouseControl.SetTooltipText(*strBuf);
2064  }
2065  }
2066  // execute action
2068  C4ScriptGuiWindowAction *action = props[actionType].GetAction();
2069  if (!action) return;
2070  action->Execute(this, player, actionType);
2071 }
2072 
2074 {
2075  assert(::MouseControl.GetPlayer() != NO_OWNER);
2076 
2077 }
2079 {
2080  assert(HasMouseFocus() && "custom menu window probably loaded incorrectly!");
2082 
2083  // needs to notify children
2084  for (C4GUI::Element *iter : *this)
2085  {
2086  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(iter);
2087  if (child->HasMouseFocus())
2088  child->OnMouseOut(player);
2089  }
2090 
2091  // execute action
2093  C4ScriptGuiWindowAction *action = props[actionType].GetAction();
2094  if (!action) return;
2095  action->Execute(this, player, actionType);
2096 }
2097 
2098 bool C4ScriptGuiWindow::MouseInput(int32_t button, int32_t mouseX, int32_t mouseY, DWORD dwKeyParam)
2099 {
2100  // only called on root
2101  assert(IsRoot());
2102  // This is only called during a mouse move event, where the MouseControl's player is available.
2103  const int32_t &player = ::MouseControl.GetPlayer();
2104  assert(player != NO_OWNER);
2105  // Only allow one window to catch the mouse input.
2106  // Do not simply return, however, since other windows might need OnMouseOut().
2107  bool oneActionAlreadyExecuted = false;
2108  // non-multiple-windows have a higher priority
2109  // this is important since they are also drawn on top
2110  for (int withMultipleFlag = 0; withMultipleFlag <= 1; ++withMultipleFlag)
2111  {
2112  for (auto iter = rbegin(); iter != rend(); ++iter)
2113  {
2114  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(*iter);
2115 
2116  const int32_t &style = child->props[C4ScriptGuiWindowPropertyName::style].GetInt();
2117  if ((withMultipleFlag == 0) && (style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
2118  if ((withMultipleFlag == 1) && !(style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
2119 
2120  // Do the visibility check first. The child itself won't do it, because we are handling mouse in/out here, too.
2121  if (!child->IsVisibleTo(player)) continue;
2122 
2123  // we are root, we have to adjust the position for the "main" windows that are centered
2124  int32_t adjustedMouseX = 0, adjustedMouseY = mouseY;
2125  int32_t offsetX = 0, offsetY = 0;
2126  if (withMultipleFlag == 0)
2127  {
2128  offsetX = -rcBounds.x;
2129  offsetY = -rcBounds.y;
2130  }
2131 
2132  adjustedMouseX = mouseX + offsetX;
2133  adjustedMouseY = mouseY + offsetY;
2134 
2135  int32_t childLeft = child->rcBounds.x;
2136  int32_t childRight = child->rcBounds.x + child->rcBounds.Wdt;
2137  int32_t childTop = child->rcBounds.y;
2138  int32_t childBottom = child->rcBounds.y + child->rcBounds.Hgt;
2139 
2140  bool inArea = true;
2141  if ((adjustedMouseX < childLeft) || (adjustedMouseX > childRight)) inArea = false;
2142  else if ((adjustedMouseY < childTop) || (adjustedMouseY > childBottom)) inArea = false;
2143 
2144  if (!inArea) // notify child if it had mouse focus before
2145  {
2146  if (child->HasMouseFocus())
2147  child->OnMouseOut(player);
2148  continue;
2149  }
2150  // Don't break since some more OnMouseOut might be necessary
2151  if (oneActionAlreadyExecuted) continue;
2152 
2153 
2154  // keep the mouse coordinates relative to the child's bounds
2155  if (child->ProcessMouseInput(button, adjustedMouseX - childLeft, adjustedMouseY - childTop - iScrollY, dwKeyParam, childLeft - offsetX, childTop + iScrollY - offsetY))
2156  {
2157  oneActionAlreadyExecuted = true;
2158  }
2159  }
2160  }
2161 
2162  return oneActionAlreadyExecuted;
2163 }
2164 
2165 bool C4ScriptGuiWindow::ProcessMouseInput(int32_t button, int32_t mouseX, int32_t mouseY, DWORD dwKeyParam, int32_t parentOffsetX, int32_t parentOffsetY)
2166 {
2167  const int32_t &player = ::MouseControl.GetPlayer();
2168  assert(player != NO_OWNER);
2169 
2170  // completely ignore mouse if the appropriate flag is set
2171  const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
2173  return false;
2174 
2175  // we have mouse focus! Is this new?
2176  if (!HasMouseFocus())
2177  OnMouseIn(player, parentOffsetX, parentOffsetY);
2178 
2179  // Make sure the UI does not catch release events without matching key-down events.
2180  // Otherwise, you could e.g. open a menu on left-down and then the menu would block the left-up event, leading to issues.
2181  if (button == C4MC_Button_LeftUp)
2182  {
2183  // Do not catch up-events without prior down-events!
2184  if (!(currentMouseState & MouseState::MouseDown)) return false;
2185  }
2186 
2187  // do not simply break the loop since some OnMouseOut might go missing
2188  bool oneActionAlreadyExecuted = false;
2189 
2190  const int32_t scrollAdjustedMouseY = mouseY + iScrollY;
2191 
2192  // children actually have a higher priority
2193  bool overChild = false; // remember for later, catch all actions that are in theory over children, even if not reaction (if main window)
2194  // use reverse iterator since children with higher Priority appear later in the list
2195  for (auto iter = rbegin(); iter != rend(); ++iter)
2196  {
2197  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(*iter);
2198 
2199  // Do the visibility check first. The child itself won't do it, because we are handling mouse in/out here, too.
2200  if (!child->IsVisibleTo(player)) continue;
2201 
2202  const int32_t childLeft = child->rcBounds.x;
2203  const int32_t childRight = child->rcBounds.x + child->rcBounds.Wdt;
2204  const int32_t childTop = child->rcBounds.y;
2205  const int32_t childBottom = child->rcBounds.y + child->rcBounds.Hgt;
2206 
2207  bool inArea = true;
2208  if ((mouseX <= childLeft) || (mouseX > childRight)) inArea = false;
2209  else if ((scrollAdjustedMouseY <= childTop) || (scrollAdjustedMouseY > childBottom)) inArea = false;
2210 
2211  if (!inArea) // notify child if it had mouse focus before
2212  {
2213  if (child->HasMouseFocus())
2214  child->OnMouseOut(player);
2215  continue;
2216  }
2217 
2218  if (oneActionAlreadyExecuted) continue;
2219 
2220  overChild = true;
2221  // keep coordinates relative to children
2222  if (child->ProcessMouseInput(button, mouseX - childLeft, scrollAdjustedMouseY - childTop, dwKeyParam, parentOffsetX + rcBounds.x, parentOffsetY + rcBounds.y - iScrollY))
2223  {
2224  oneActionAlreadyExecuted = true;
2225  }
2226  }
2227 
2228  if (oneActionAlreadyExecuted) return true;
2229 
2230  //C4GUI::Element::MouseInput(rMouse, button, mouseX, mouseY, dwKeyParam);
2231 
2232  // remember button-down events. The action will only be executed on button-up
2233  // The sequence for double-clicks is LeftDown-LeftUp-LeftDouble-LeftUp, so treat double as down
2234  if (button == C4MC_Button_LeftDown || button == C4MC_Button_LeftDouble)
2235  currentMouseState |= MouseState::MouseDown;
2236  // trigger!
2237  if (button == C4MC_Button_LeftUp)
2238  {
2241  if (action)
2242  {
2244  return true;
2245  }
2246  }
2247 
2248  // for scroll-enabled windows, scroll contents with wheel
2249  if (pScrollBar->IsVisible() && (button == C4MC_Button_Wheel))
2250  {
2251  short delta = (short)(dwKeyParam >> 16);
2252  ScrollBy(-delta);
2253  //float fac = (lastDrawPosition.bottomMostChild - lastDrawPosition.topMostChild);
2254  //if (fac == 0.0f) fac = 1.0f;
2255  //scrollBar->ScrollBy(-float(delta) / fac);
2256  return true;
2257  }
2258 
2259  // forward to scroll-bar if in area
2260  if (pScrollBar->IsVisible())
2261  {
2262  if ((mouseX > pScrollBar->rcBounds.x && mouseX < pScrollBar->rcBounds.x + pScrollBar->rcBounds.Wdt)
2263  && (mouseY > pScrollBar->rcBounds.y && mouseY < pScrollBar->rcBounds.y + pScrollBar->rcBounds.Hgt))
2264  {
2265  C4GUI::CMouse mouse(mouseX, mouseY);
2266  if (::MouseControl.IsLeftDown()) mouse.LDown = true;
2267  pScrollBar->MouseInput(mouse, button, mouseX - pScrollBar->rcBounds.x, mouseY - pScrollBar->rcBounds.y, dwKeyParam);
2268  }
2269  }
2270 
2271 
2272  // if the user still clicked on a menu - even if it didn't do anything, catch it
2273  // but do that only on the top-level to not stop traversing other branches
2274  if (isMainWindow)
2275  return overChild;
2276  return false;
2277 }
2278 
2279 bool C4ScriptGuiWindow::ExecuteCommand(int32_t actionID, int32_t player, int32_t subwindowID, int32_t actionType, C4Object *target)
2280 {
2281  if (isMainWindow && subwindowID) // we are a main window! try a shortcut through the ID?
2282  {
2283  MenuDebugLogF("passing command... instance:%d, plr:%d, subwin:%d, type:%d [I am %d, MW]", actionID, player, subwindowID, actionType, id);
2284  MenuDebugLogF("stats:");
2285  MenuDebugLogF("active menus:\t%d", GetElementCount());
2286  MenuDebugLogF("children ID map:\t%d", childrenIDMap.size());
2287  // the reasoning for that shortcut is that I assume that usually windows with actions will also have an ID assigned
2288  // this obviously doesn't have to be the case, but I believe it's worth the try
2289  std::pair<std::multimap<int32_t, C4ScriptGuiWindow*>::iterator, std::multimap<int32_t, C4ScriptGuiWindow*>::iterator> range;
2290  range = childrenIDMap.equal_range(subwindowID);
2291 
2292  for (std::multimap<int32_t, C4ScriptGuiWindow*>::iterator iter = range.first; iter != range.second; ++iter)
2293  {
2294  if (iter->second->ExecuteCommand(actionID, player, subwindowID, actionType, target))
2295  {
2296  MenuDebugLogF("shortcutting command sucessful!");
2297  return true;
2298  }
2299  }
2300  // it is not possible that another window would match the criteria. Abort later after self-check
2301  MenuDebugLogF("shortcutting command failed.. no appropriate window");
2302  }
2303 
2304  // are we elligible?
2305  if ((id == subwindowID) && (this->target == target))
2306  {
2307  MenuDebugLogF("stats: (I am %d)", id);
2308  MenuDebugLogF("children:\t%d", GetElementCount());
2309  MenuDebugLogF("all actions:\t%d", props[actionType].GetAllActions().size());
2310  std::list<C4ScriptGuiWindowAction*> allActions = props[actionType].GetAllActions();
2311  for (auto action : allActions)
2312  {
2313  assert(action && "C4ScriptGuiWindowProperty::GetAllActions returned list with null-pointer");
2314 
2315  if (action->ExecuteCommand(actionID, this, player))
2316  {
2317  MenuDebugLogF("executing command sucessful!");
2318  return true;
2319  }
2320  }
2321 
2322  // note that we should not simply return false here
2323  // there is no guarantee that only one window with that target&ID exists
2324  }
2325 
2326  // not caught, forward to children!
2327  // abort if main window, though. See above
2328  if (isMainWindow && subwindowID)
2329  {
2330  MenuDebugLogF("executing command failed!");
2331  return false;
2332  }
2333 
2334  // otherwise, just pass to children..
2335  for (C4GUI::Element *element : *this)
2336  {
2337  C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
2338  if (child->ExecuteCommand(actionID, player, subwindowID, actionType, target))
2339  {
2340  MenuDebugLogF("passing command sucessful! (I am %d - &p)", id, this->target);
2341  return true;
2342  }
2343  }
2344  return false;
2345 }
2346 
2347 bool C4ScriptGuiWindow::IsRoot()
2348 {
2349  return this == Game.ScriptGuiRoot.get();
2350 }
2351 
2353 {
2354  // Not visible at all?
2355  if (!IsVisible()) return false;
2356  // We have a player assigned and it's a different one?
2357  const int32_t &myPlayer = props[C4ScriptGuiWindowPropertyName::player].GetInt();
2358  if (myPlayer != ANY_OWNER && player != myPlayer) return false;
2359  // We have a target object which is invisible to the player?
2360  if (target && !target->IsVisible(player, false)) return false;
2361  // Default to visible!
2362  return true;
2363 }
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
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:138
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:667
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:685
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)
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:757
int32_t iClientHeight
Definition: C4Gui.h:944
Iterator begin()
virtual const char * GetName() const
Definition: C4PropList.cpp:649
C4GraphicsSystem GraphicsSystem
Definition: C4Globals.cpp:51
virtual C4ValueArray * GetProperties() const
Definition: C4PropList.cpp:914
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:597
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:137
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:971
int32_t Hgt
Definition: C4Rect.h:30
std::unique_ptr< C4ScriptGuiWindow > ScriptGuiRoot
Definition: C4Game.h:234
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:260
const C4Value ToC4Value()
float CalculateRelativeSize(float parentWidthOrHeight, C4ScriptGuiWindowPropertyName::type absoluteProperty, C4ScriptGuiWindowPropertyName::type relativeProperty)
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:4052
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