forked from Mirrors/openclonk
2364 lines
80 KiB
C++
2364 lines
80 KiB
C++
/*
|
|
* OpenClonk, http://www.openclonk.org
|
|
*
|
|
* Copyright (c) 2014-2016, The OpenClonk Team and contributors
|
|
*
|
|
* Distributed under the terms of the ISC license; see accompanying file
|
|
* "COPYING" for details.
|
|
*
|
|
* "Clonk" is a registered trademark of Matthes Bender, used with permission.
|
|
* See accompanying file "TRADEMARK" for details.
|
|
*
|
|
* To redistribute this file separately, substitute the full license texts
|
|
* for the above references.
|
|
*/
|
|
|
|
/*
|
|
A flexible ingame menu system that can be used to compose large GUIs out of multiple windows.
|
|
|
|
Every window is basically a rectangle that can contain some make-up-information (symbol/text/...) and coordinates.
|
|
Those coordinates can either be relative to the window's parent or in total pixels or a mixture of both.
|
|
|
|
The entry point for all of the callbacks for mouse input, drawing, etc. is one normal window which always exists and happens
|
|
to be the parent of ALL of the script-created menus. Callbacks are usually forwarded to the children.
|
|
|
|
If you want to add new window properties (similar to backgroundColor, onClickAction etc.) you have to make sure that they are
|
|
serialized correctly and cleaned up if necessary when a menu window is closed or the property is overwritten by a script call!
|
|
*/
|
|
|
|
#include "C4Include.h"
|
|
#include "gui/C4ScriptGuiWindow.h"
|
|
|
|
#include "control/C4Control.h"
|
|
#include "game/C4Application.h"
|
|
#include "game/C4GraphicsSystem.h"
|
|
#include "game/C4Viewport.h"
|
|
#include "graphics/C4Draw.h"
|
|
#include "graphics/C4GraphicsResource.h"
|
|
#include "gui/C4MouseControl.h"
|
|
#include "lib/StdColors.h"
|
|
#include "object/C4Def.h"
|
|
#include "object/C4DefList.h"
|
|
#include "object/C4Object.h"
|
|
#include "player/C4Player.h"
|
|
#include "player/C4PlayerList.h"
|
|
|
|
// Adds some helpful logs for hunting control & menu based desyncs.
|
|
//#define MenuDebugLogF(...) DebugLogF(__VA_ARGS__)
|
|
#define MenuDebugLogF(...) ((void)0)
|
|
|
|
// This in in EM! Also, golden ratio
|
|
const float C4ScriptGuiWindow::standardWidth = 50.0f;
|
|
const float C4ScriptGuiWindow::standardHeight = 31.0f;
|
|
|
|
float C4ScriptGuiWindow::Em2Pix(float em)
|
|
{
|
|
return static_cast<float>(::GraphicsResource.FontRegular.GetFontHeight()) * em;
|
|
}
|
|
|
|
float C4ScriptGuiWindow::Pix2Em(float pix)
|
|
{
|
|
return pix / static_cast<float>(std::max<int32_t>(1, ::GraphicsResource.FontRegular.GetFontHeight()));
|
|
}
|
|
|
|
C4ScriptGuiWindowAction::~C4ScriptGuiWindowAction()
|
|
{
|
|
if (text)
|
|
text->DecRef();
|
|
if (nextAction)
|
|
delete nextAction;
|
|
}
|
|
|
|
const C4Value C4ScriptGuiWindowAction::ToC4Value(bool first)
|
|
{
|
|
C4ValueArray *array = new C4ValueArray();
|
|
|
|
switch (action)
|
|
{
|
|
case C4ScriptGuiWindowActionID::Call:
|
|
array->SetSize(4);
|
|
array->SetItem(0, C4Value(action));
|
|
array->SetItem(1, C4Value(target));
|
|
array->SetItem(2, C4Value(text));
|
|
array->SetItem(3, value);
|
|
break;
|
|
|
|
case C4ScriptGuiWindowActionID::SetTag:
|
|
array->SetSize(4);
|
|
array->SetItem(0, C4Value(action));
|
|
array->SetItem(1, C4Value(text));
|
|
array->SetItem(2, C4Value(subwindowID));
|
|
array->SetItem(3, C4Value(target));
|
|
break;
|
|
|
|
case 0: // can actually happen if the action is invalidated
|
|
break;
|
|
|
|
default:
|
|
assert(false && "trying to save C4ScriptGuiWindowAction without valid action");
|
|
break;
|
|
}
|
|
|
|
assert (array->GetSize() < 6);
|
|
array->SetSize(6);
|
|
array->SetItem(5, C4Value(id));
|
|
|
|
if (!first || !nextAction) return C4Value(array);
|
|
|
|
// this action is the first in a chain of actions
|
|
// all following actions (and this one) have to be put into another array
|
|
C4ValueArray *container = new C4ValueArray();
|
|
int32_t size = 1;
|
|
container->SetSize(size);
|
|
container->SetItem(0, C4Value(array));
|
|
|
|
C4ScriptGuiWindowAction *next = nextAction;
|
|
while (next)
|
|
{
|
|
C4Value val = next->ToC4Value(false);
|
|
++size;
|
|
container->SetSize(size);
|
|
container->SetItem(size - 1, val);
|
|
next = next->nextAction;
|
|
}
|
|
return C4Value(container);
|
|
}
|
|
|
|
void C4ScriptGuiWindowAction::ClearPointers(C4Object *pObj)
|
|
{
|
|
C4Object *targetObj = target ? target->GetObject() : nullptr;
|
|
|
|
if (targetObj == pObj)
|
|
{
|
|
// not only forget object, but completely invalidate action
|
|
action = 0;
|
|
target = nullptr;
|
|
}
|
|
if (nextAction)
|
|
nextAction->ClearPointers(pObj);
|
|
}
|
|
bool C4ScriptGuiWindowAction::Init(C4ValueArray *array, int32_t index)
|
|
{
|
|
if (array->GetSize() == 0) // safety
|
|
return false;
|
|
|
|
// an array of actions?
|
|
if (array->GetItem(0).getArray())
|
|
{
|
|
// add action to action chain?
|
|
if (index+1 < array->GetSize())
|
|
{
|
|
nextAction = new C4ScriptGuiWindowAction();
|
|
nextAction->Init(array, index + 1);
|
|
}
|
|
// continue with one sub array
|
|
array = array->GetItem(index).getArray();
|
|
if (!array) return false;
|
|
}
|
|
// retrieve type of action
|
|
int newAction = array->GetItem(0).getInt();
|
|
action = 0; // still invalid!
|
|
|
|
// when loading, the array has a size of 6 with the 5th element being the ID
|
|
if (array->GetSize() == 6)
|
|
id = array->GetItem(3).getInt();
|
|
|
|
switch (newAction)
|
|
{
|
|
case C4ScriptGuiWindowActionID::Call:
|
|
if (array->GetSize() < 3) return false;
|
|
target = array->GetItem(1).getPropList();
|
|
text = array->GetItem(2).getStr();
|
|
if (!target || !text) return false;
|
|
if (array->GetSize() >= 4)
|
|
value = C4Value(array->GetItem(3));
|
|
text->IncRef();
|
|
|
|
// important! needed to identify actions later!
|
|
if (!id)
|
|
{
|
|
id = ::Game.ScriptGuiRoot->GenerateActionID();
|
|
MenuDebugLogF("assigning action ID %d\t\taction:%d, text:%s", id, newAction, text->GetCStr());
|
|
}
|
|
|
|
break;
|
|
|
|
case C4ScriptGuiWindowActionID::SetTag:
|
|
if (array->GetSize() < 4) return false;
|
|
text = array->GetItem(1).getStr();
|
|
if (!text) return false;
|
|
text->IncRef();
|
|
subwindowID = array->GetItem(2).getInt();
|
|
target = array->GetItem(3).getObj(); // getObj on purpose. Need to validate that.
|
|
break;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
action = newAction;
|
|
return true;
|
|
}
|
|
|
|
void C4ScriptGuiWindowAction::Execute(C4ScriptGuiWindow *parent, int32_t player, int32_t actionType)
|
|
{
|
|
assert(parent && "C4ScriptGuiWindow::Execute must always be called with parent");
|
|
MenuDebugLogF("Excuting action (nextAction: %x, subwID: %d, target: %x, text: %s, type: %d)", nextAction, subwindowID, target, text->GetCStr(), actionType);
|
|
|
|
// invalid ID? can be set by removal of target object
|
|
if (action)
|
|
{
|
|
// get menu main window
|
|
C4ScriptGuiWindow *main = parent;
|
|
C4ScriptGuiWindow *from = main;
|
|
while (!from->IsRoot())
|
|
{
|
|
main = from;
|
|
from = static_cast<C4ScriptGuiWindow*>(from->GetParent());
|
|
}
|
|
|
|
switch (action)
|
|
{
|
|
case C4ScriptGuiWindowActionID::Call:
|
|
{
|
|
if (!target) // ohject removed in the meantime?
|
|
break;
|
|
MenuDebugLogF("[ACTION REQUEST] action /call/");
|
|
// the action needs to be synchronized! Assemble command and put it into control queue!
|
|
Game.Input.Add(CID_MenuCommand, new C4ControlMenuCommand(id, player, main->GetID(), parent->GetID(), parent->target, actionType));
|
|
break;
|
|
}
|
|
|
|
case C4ScriptGuiWindowActionID::SetTag:
|
|
{
|
|
C4ScriptGuiWindow *window = main;
|
|
if (subwindowID == 0)
|
|
window = parent;
|
|
else if (subwindowID > 0)
|
|
{
|
|
C4Object *targetObj = dynamic_cast<C4Object*> (target);
|
|
window = main->GetSubWindow(subwindowID, targetObj);
|
|
}
|
|
if (window)
|
|
window->SetTag(text);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
assert(false && "C4ScriptGuiWindowAction without valid or invalidated ID");
|
|
break;
|
|
}
|
|
} // action
|
|
|
|
if (nextAction)
|
|
{
|
|
nextAction->Execute(parent, player, actionType);
|
|
}
|
|
}
|
|
|
|
bool C4ScriptGuiWindowAction::ExecuteCommand(int32_t actionID, C4ScriptGuiWindow *parent, int32_t player)
|
|
{
|
|
MenuDebugLogF("checking action %d (==%d?)\t\tmy action: %d", id, actionID, action);
|
|
// target has already been checked for validity
|
|
if (id == actionID && action)
|
|
{
|
|
assert(action == C4ScriptGuiWindowActionID::Call && "C4ControlMenuCommand for invalid action!");
|
|
|
|
// get menu main window
|
|
C4ScriptGuiWindow *main = parent;
|
|
C4ScriptGuiWindow *from = main;
|
|
while (!from->IsRoot())
|
|
{
|
|
main = from;
|
|
from = static_cast<C4ScriptGuiWindow*>(from->GetParent());
|
|
}
|
|
MenuDebugLogF("command synced.. target: %x, targetObj: %x, func: %s", target, target->GetObject(), text->GetCStr());
|
|
C4AulParSet Pars(value, C4VInt(player), C4VInt(main->GetID()), C4VInt(parent->GetID()), (parent->target && parent->target->Status) ? C4VObj(parent->target) : C4VNull);
|
|
target->Call(text->GetCStr(), &Pars);
|
|
return true;
|
|
}
|
|
if (nextAction)
|
|
return nextAction->ExecuteCommand(actionID, parent, player);
|
|
return false;
|
|
}
|
|
|
|
C4ScriptGuiWindowProperty::~C4ScriptGuiWindowProperty()
|
|
{
|
|
// is cleaned up from destructor of C4ScriptGuiWindow
|
|
}
|
|
|
|
void C4ScriptGuiWindowProperty::SetInt(int32_t to, C4String *tag)
|
|
{
|
|
if (!tag) tag = &Strings.P[P_Std];
|
|
taggedProperties[tag] = Prop();
|
|
current = &taggedProperties[tag];
|
|
current->d = to;
|
|
}
|
|
void C4ScriptGuiWindowProperty::SetFloat(float to, C4String *tag)
|
|
{
|
|
if (!tag) tag = &Strings.P[P_Std];
|
|
taggedProperties[tag] = Prop();
|
|
current = &taggedProperties[tag];
|
|
current->f = to;
|
|
}
|
|
void C4ScriptGuiWindowProperty::SetNull(C4String *tag)
|
|
{
|
|
if (!tag) tag = &Strings.P[P_Std];
|
|
taggedProperties[tag] = Prop();
|
|
current = &taggedProperties[tag];
|
|
current->data = nullptr;
|
|
}
|
|
|
|
void C4ScriptGuiWindowProperty::CleanUp(Prop &prop)
|
|
{
|
|
switch (type)
|
|
{
|
|
case C4ScriptGuiWindowPropertyName::frameDecoration:
|
|
if (prop.deco) delete prop.deco;
|
|
break;
|
|
case C4ScriptGuiWindowPropertyName::onClickAction:
|
|
case C4ScriptGuiWindowPropertyName::onMouseInAction:
|
|
case C4ScriptGuiWindowPropertyName::onMouseOutAction:
|
|
case C4ScriptGuiWindowPropertyName::onCloseAction:
|
|
if (prop.action) delete prop.action;
|
|
break;
|
|
case C4ScriptGuiWindowPropertyName::text:
|
|
case C4ScriptGuiWindowPropertyName::tooltip:
|
|
case C4ScriptGuiWindowPropertyName::symbolGraphicsName:
|
|
if (prop.strBuf) delete prop.strBuf;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void C4ScriptGuiWindowProperty::CleanUpAll()
|
|
{
|
|
for (auto & taggedProperty : taggedProperties)
|
|
{
|
|
CleanUp(taggedProperty.second);
|
|
if (taggedProperty.first != &Strings.P[P_Std])
|
|
taggedProperty.first->DecRef();
|
|
}
|
|
}
|
|
|
|
const C4Value C4ScriptGuiWindowProperty::ToC4Value()
|
|
{
|
|
C4PropList *proplist = nullptr;
|
|
|
|
bool onlyOneTag = taggedProperties.size() == 1;
|
|
if (!onlyOneTag) // we will need a tagged proplist
|
|
proplist = C4PropList::New();
|
|
|
|
// go through all of the tagged properties and add a property to the proplist containing both the tag name
|
|
// and the serialzed C4Value of the properties' value
|
|
for(auto & taggedProperty : taggedProperties)
|
|
{
|
|
C4String *tagString = taggedProperty.first;
|
|
const Prop &prop = taggedProperty.second;
|
|
|
|
C4Value val;
|
|
|
|
// get value to save
|
|
switch (type)
|
|
{
|
|
case C4ScriptGuiWindowPropertyName::left:
|
|
case C4ScriptGuiWindowPropertyName::right:
|
|
case C4ScriptGuiWindowPropertyName::top:
|
|
case C4ScriptGuiWindowPropertyName::bottom:
|
|
case C4ScriptGuiWindowPropertyName::relLeft:
|
|
case C4ScriptGuiWindowPropertyName::relRight:
|
|
case C4ScriptGuiWindowPropertyName::relTop:
|
|
case C4ScriptGuiWindowPropertyName::relBottom:
|
|
case C4ScriptGuiWindowPropertyName::leftMargin:
|
|
case C4ScriptGuiWindowPropertyName::rightMargin:
|
|
case C4ScriptGuiWindowPropertyName::topMargin:
|
|
case C4ScriptGuiWindowPropertyName::bottomMargin:
|
|
case C4ScriptGuiWindowPropertyName::relLeftMargin:
|
|
case C4ScriptGuiWindowPropertyName::relRightMargin:
|
|
case C4ScriptGuiWindowPropertyName::relTopMargin:
|
|
case C4ScriptGuiWindowPropertyName::relBottomMargin:
|
|
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.");
|
|
break;
|
|
|
|
case C4ScriptGuiWindowPropertyName::backgroundColor:
|
|
case C4ScriptGuiWindowPropertyName::style:
|
|
case C4ScriptGuiWindowPropertyName::priority:
|
|
case C4ScriptGuiWindowPropertyName::player:
|
|
val = C4Value(prop.d);
|
|
break;
|
|
|
|
case C4ScriptGuiWindowPropertyName::symbolObject:
|
|
val = C4Value(prop.obj);
|
|
break;
|
|
|
|
case C4ScriptGuiWindowPropertyName::symbolDef:
|
|
val = C4Value(prop.def);
|
|
break;
|
|
|
|
case C4ScriptGuiWindowPropertyName::frameDecoration:
|
|
if (prop.deco)
|
|
val = C4Value(prop.deco->pSourceDef);
|
|
break;
|
|
|
|
case C4ScriptGuiWindowPropertyName::text:
|
|
case C4ScriptGuiWindowPropertyName::symbolGraphicsName:
|
|
case C4ScriptGuiWindowPropertyName::tooltip:
|
|
{
|
|
if (prop.strBuf)
|
|
{
|
|
// string existing?
|
|
C4String *s = Strings.FindString(prop.strBuf->getData());
|
|
if (!s) s = Strings.RegString(prop.strBuf->getData());
|
|
val = C4Value(s);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case C4ScriptGuiWindowPropertyName::onClickAction:
|
|
case C4ScriptGuiWindowPropertyName::onMouseInAction:
|
|
case C4ScriptGuiWindowPropertyName::onMouseOutAction:
|
|
case C4ScriptGuiWindowPropertyName::onCloseAction:
|
|
if (prop.action)
|
|
val = prop.action->ToC4Value();
|
|
break;
|
|
|
|
default:
|
|
assert(false && "C4ScriptGuiWindowAction should never have undefined type");
|
|
break;
|
|
} // switch
|
|
|
|
if (onlyOneTag) return val;
|
|
assert(proplist);
|
|
proplist->SetPropertyByS(tagString, val);
|
|
}
|
|
|
|
return C4Value(proplist);
|
|
}
|
|
|
|
void C4ScriptGuiWindowProperty::Set(const C4Value &value, C4String *tag)
|
|
{
|
|
C4PropList *proplist = value.getPropList();
|
|
bool isTaggedPropList = false;
|
|
if (proplist)
|
|
isTaggedPropList = !(proplist->GetDef() || proplist->GetObject());
|
|
|
|
if (isTaggedPropList)
|
|
{
|
|
std::unique_ptr<C4ValueArray> properties(proplist->GetProperties());
|
|
properties->SortStrings();
|
|
for (int32_t i = 0; i < properties->GetSize(); ++i)
|
|
{
|
|
const C4Value &entry = properties->GetItem(i);
|
|
C4String *key = entry.getStr();
|
|
assert(key && "Proplist returns non-string as key");
|
|
|
|
C4Value property;
|
|
proplist->GetPropertyByS(key, &property);
|
|
Set(property, key);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// special treatment for some that have to be deleted (due to owning string/frame deco/...)
|
|
if (taggedProperties.count(tag))
|
|
CleanUp(taggedProperties[tag]);
|
|
else // new tag, retain the proplist if not standard
|
|
if (tag != &Strings.P[P_Std])
|
|
tag->IncRef();
|
|
|
|
taggedProperties[tag] = Prop();
|
|
// in order to make /current/ sane, always reset it - not relying on implementation details of std::map
|
|
// if the user wants a special tag selected, he should do that (standard selection will still be "Std")
|
|
current = &taggedProperties[tag];
|
|
currentTag = tag;
|
|
|
|
|
|
// now that a new property entry has been created and the old has been cleaned up, get the data from the C4Value
|
|
switch (type)
|
|
{
|
|
case C4ScriptGuiWindowPropertyName::left:
|
|
case C4ScriptGuiWindowPropertyName::right:
|
|
case C4ScriptGuiWindowPropertyName::top:
|
|
case C4ScriptGuiWindowPropertyName::bottom:
|
|
case C4ScriptGuiWindowPropertyName::relLeft:
|
|
case C4ScriptGuiWindowPropertyName::relRight:
|
|
case C4ScriptGuiWindowPropertyName::relTop:
|
|
case C4ScriptGuiWindowPropertyName::relBottom:
|
|
case C4ScriptGuiWindowPropertyName::leftMargin:
|
|
case C4ScriptGuiWindowPropertyName::rightMargin:
|
|
case C4ScriptGuiWindowPropertyName::topMargin:
|
|
case C4ScriptGuiWindowPropertyName::bottomMargin:
|
|
case C4ScriptGuiWindowPropertyName::relLeftMargin:
|
|
case C4ScriptGuiWindowPropertyName::relRightMargin:
|
|
case C4ScriptGuiWindowPropertyName::relTopMargin:
|
|
case C4ScriptGuiWindowPropertyName::relBottomMargin:
|
|
assert (false && "Trying to set positional properties directly. Those should always come parsed from a string.");
|
|
break;
|
|
|
|
case C4ScriptGuiWindowPropertyName::backgroundColor:
|
|
case C4ScriptGuiWindowPropertyName::style:
|
|
case C4ScriptGuiWindowPropertyName::priority:
|
|
current->d = value.getInt();
|
|
break;
|
|
|
|
case C4ScriptGuiWindowPropertyName::player:
|
|
if (value == C4VNull)
|
|
current->d = ANY_OWNER;
|
|
else
|
|
current->d = value.getInt();
|
|
break;
|
|
|
|
case C4ScriptGuiWindowPropertyName::symbolObject:
|
|
{
|
|
C4PropList *symbol = value.getPropList();
|
|
if (symbol)
|
|
current->obj = symbol->GetObject();
|
|
else current->obj = nullptr;
|
|
break;
|
|
}
|
|
case C4ScriptGuiWindowPropertyName::symbolDef:
|
|
{
|
|
C4PropList *symbol = value.getPropList();
|
|
if (symbol)
|
|
current->def = symbol->GetDef();
|
|
else current->def = nullptr;
|
|
break;
|
|
}
|
|
case C4ScriptGuiWindowPropertyName::frameDecoration:
|
|
{
|
|
C4Def *def = value.getDef();
|
|
|
|
if (def)
|
|
{
|
|
current->deco = new C4GUI::FrameDecoration();
|
|
if (!current->deco->SetByDef(def))
|
|
{
|
|
delete current->deco;
|
|
current->deco = nullptr;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case C4ScriptGuiWindowPropertyName::text:
|
|
case C4ScriptGuiWindowPropertyName::symbolGraphicsName:
|
|
case C4ScriptGuiWindowPropertyName::tooltip:
|
|
{
|
|
C4String *string = value.getStr();
|
|
StdCopyStrBuf *buf = new StdCopyStrBuf();
|
|
if (string)
|
|
buf->Copy(string->GetCStr());
|
|
else buf->Copy("");
|
|
current->strBuf = buf;
|
|
break;
|
|
}
|
|
case C4ScriptGuiWindowPropertyName::onClickAction:
|
|
case C4ScriptGuiWindowPropertyName::onMouseInAction:
|
|
case C4ScriptGuiWindowPropertyName::onMouseOutAction:
|
|
case C4ScriptGuiWindowPropertyName::onCloseAction:
|
|
{
|
|
C4ValueArray *array = value.getArray();
|
|
if (array)
|
|
{
|
|
assert (!current->action && "Prop() contains action prior to assignment");
|
|
current->action = new C4ScriptGuiWindowAction();
|
|
current->action->Init(array);
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
assert(false && "C4ScriptGuiWindowAction should never have undefined type");
|
|
break;
|
|
} // switch
|
|
}
|
|
|
|
void C4ScriptGuiWindowProperty::ClearPointers(C4Object *pObj)
|
|
{
|
|
// assume that we actually contain an object
|
|
// go through all the tags and, in case the tag has anything to do with objects, check and clear it
|
|
for (auto & taggedProperty : taggedProperties)
|
|
{
|
|
switch (type)
|
|
{
|
|
case C4ScriptGuiWindowPropertyName::symbolObject:
|
|
if (taggedProperty.second.obj == pObj)
|
|
taggedProperty.second.obj = nullptr;
|
|
break;
|
|
|
|
case C4ScriptGuiWindowPropertyName::onClickAction:
|
|
case C4ScriptGuiWindowPropertyName::onMouseInAction:
|
|
case C4ScriptGuiWindowPropertyName::onMouseOutAction:
|
|
case C4ScriptGuiWindowPropertyName::onCloseAction:
|
|
if (taggedProperty.second.action)
|
|
taggedProperty.second.action->ClearPointers(pObj);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool C4ScriptGuiWindowProperty::SwitchTag(C4String *tag)
|
|
{
|
|
if (!taggedProperties.count(tag)) return false; // tag not available
|
|
if (current == &taggedProperties[tag]) return false; // tag already set?
|
|
current = &taggedProperties[tag];
|
|
currentTag = tag;
|
|
return true;
|
|
}
|
|
|
|
std::list<C4ScriptGuiWindowAction*> C4ScriptGuiWindowProperty::GetAllActions()
|
|
{
|
|
std::list<C4ScriptGuiWindowAction*> allActions;
|
|
for (auto & taggedProperty : taggedProperties)
|
|
{
|
|
Prop &p = taggedProperty.second;
|
|
if (p.action)
|
|
allActions.push_back(p.action);
|
|
}
|
|
return allActions;
|
|
}
|
|
|
|
|
|
C4ScriptGuiWindow::C4ScriptGuiWindow() : C4GUI::ScrollWindow(this)
|
|
{
|
|
Init();
|
|
}
|
|
|
|
void C4ScriptGuiWindow::Init()
|
|
{
|
|
id = 0;
|
|
name = nullptr;
|
|
|
|
isMainWindow = false;
|
|
mainWindowNeedsLayoutUpdate = false;
|
|
|
|
// properties must know what they stand for
|
|
for (int32_t i = 0; i < C4ScriptGuiWindowPropertyName::_lastProp; ++i)
|
|
props[i].type = i;
|
|
|
|
// standard values for all of the properties
|
|
|
|
// exact offsets are standard 0
|
|
props[C4ScriptGuiWindowPropertyName::left].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::right].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::top].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::bottom].SetNull();
|
|
// relative offsets are standard full screen 0,0 - 1,1
|
|
props[C4ScriptGuiWindowPropertyName::relLeft].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::relTop].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::relBottom].SetFloat(1.0f);
|
|
props[C4ScriptGuiWindowPropertyName::relRight].SetFloat(1.0f);
|
|
// all margins are always standard 0
|
|
props[C4ScriptGuiWindowPropertyName::leftMargin].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::rightMargin].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::topMargin].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::bottomMargin].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::relLeftMargin].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::relTopMargin].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::relBottomMargin].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::relRightMargin].SetNull();
|
|
// other properties are 0
|
|
props[C4ScriptGuiWindowPropertyName::backgroundColor].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::frameDecoration].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::symbolObject].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::symbolDef].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::text].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::symbolGraphicsName].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::tooltip].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::onClickAction].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::onMouseInAction].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::onMouseOutAction].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::onCloseAction].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::style].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::priority].SetNull();
|
|
props[C4ScriptGuiWindowPropertyName::player].SetInt(ANY_OWNER);
|
|
|
|
wasRemoved = false;
|
|
closeActionWasExecuted = false;
|
|
currentMouseState = MouseState::None;
|
|
target = nullptr;
|
|
pScrollBar->fAutoHide = true;
|
|
|
|
rcBounds.x = rcBounds.y = 0;
|
|
rcBounds.Wdt = rcBounds.Hgt = 0;
|
|
}
|
|
|
|
C4ScriptGuiWindow::~C4ScriptGuiWindow()
|
|
{
|
|
ClearChildren(false);
|
|
|
|
// delete certain properties that contain allocated elements or referenced strings
|
|
for (auto & prop : props)
|
|
prop.CleanUpAll();
|
|
|
|
if (pScrollBar)
|
|
delete pScrollBar;
|
|
if (name)
|
|
name->DecRef();
|
|
}
|
|
|
|
// helper function
|
|
void C4ScriptGuiWindow::SetMarginProperties(const C4Value &property, C4String *tag)
|
|
{
|
|
// the value might be a tagged proplist again
|
|
if (property.GetType() == C4V_Type::C4V_PropList)
|
|
{
|
|
C4PropList *proplist = property.getPropList();
|
|
for (C4PropList::Iterator iter = proplist->begin(); iter != proplist->end(); ++iter)
|
|
{
|
|
SetMarginProperties(iter->Value, iter->Key);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// safety
|
|
if (property.GetType() == C4V_Type::C4V_Array && property.getArray()->GetSize() == 0)
|
|
return;
|
|
|
|
// always set all four margins
|
|
for (int i = 0; i < 4; ++i)
|
|
{
|
|
C4ScriptGuiWindowPropertyName::type relative, absolute;
|
|
switch (i)
|
|
{
|
|
case 0:
|
|
absolute = C4ScriptGuiWindowPropertyName::leftMargin;
|
|
relative = C4ScriptGuiWindowPropertyName::relLeftMargin;
|
|
break;
|
|
case 1:
|
|
absolute = C4ScriptGuiWindowPropertyName::topMargin;
|
|
relative = C4ScriptGuiWindowPropertyName::relTopMargin;
|
|
break;
|
|
case 2:
|
|
absolute = C4ScriptGuiWindowPropertyName::rightMargin;
|
|
relative = C4ScriptGuiWindowPropertyName::relRightMargin;
|
|
break;
|
|
case 3:
|
|
absolute = C4ScriptGuiWindowPropertyName::bottomMargin;
|
|
relative = C4ScriptGuiWindowPropertyName::relBottomMargin;
|
|
break;
|
|
default:
|
|
assert(false);
|
|
}
|
|
|
|
if (property.GetType() == C4V_Type::C4V_Array)
|
|
{
|
|
C4ValueArray *array = property.getArray();
|
|
int realIndex = i % array->GetSize();
|
|
SetPositionStringProperties(array->GetItem(realIndex), relative, absolute, tag);
|
|
}
|
|
else
|
|
// normal string, hopefully
|
|
SetPositionStringProperties(property, relative, absolute, tag);
|
|
}
|
|
}
|
|
|
|
C4Value C4ScriptGuiWindow::MarginsToC4Value()
|
|
{
|
|
C4ValueArray *array = new C4ValueArray();
|
|
array->SetSize(4);
|
|
|
|
array->SetItem(0, PositionToC4Value(C4ScriptGuiWindowPropertyName::relLeftMargin, C4ScriptGuiWindowPropertyName::leftMargin));
|
|
array->SetItem(1, PositionToC4Value(C4ScriptGuiWindowPropertyName::relTopMargin, C4ScriptGuiWindowPropertyName::topMargin));
|
|
array->SetItem(2, PositionToC4Value(C4ScriptGuiWindowPropertyName::relRightMargin, C4ScriptGuiWindowPropertyName::rightMargin));
|
|
array->SetItem(3, PositionToC4Value(C4ScriptGuiWindowPropertyName::relBottomMargin, C4ScriptGuiWindowPropertyName::bottomMargin));
|
|
|
|
return C4Value(array);
|
|
}
|
|
|
|
// helper function
|
|
void C4ScriptGuiWindow::SetPositionStringProperties(const C4Value &property, C4ScriptGuiWindowPropertyName::type relative, C4ScriptGuiWindowPropertyName::type absolute, C4String *tag)
|
|
{
|
|
// the value might be a tagged proplist again
|
|
if (property.GetType() == C4V_Type::C4V_PropList)
|
|
{
|
|
C4PropList *proplist = property.getPropList();
|
|
for (C4PropList::Iterator iter = proplist->begin(); iter != proplist->end(); ++iter)
|
|
{
|
|
SetPositionStringProperties(iter->Value, relative, absolute, iter->Key);
|
|
}
|
|
return;
|
|
}
|
|
// safety
|
|
if (property.GetType() != C4V_Type::C4V_String) {
|
|
if(property.GetType() != C4V_Type::C4V_Nil)
|
|
LogF("Warning: Got %s instead of expected menu format string.", property.GetTypeName());
|
|
return;
|
|
}
|
|
|
|
float relativeValue = 0.0;
|
|
float absoluteValue = 0.0;
|
|
|
|
std::locale c_locale("C");
|
|
std::istringstream reader(std::string(property.getStr()->GetCStr()));
|
|
reader.imbue(c_locale);
|
|
if(!reader.good()) return;
|
|
|
|
while (!reader.eof())
|
|
{
|
|
reader >> std::ws; // eat white space
|
|
|
|
// look for next float
|
|
float value;
|
|
// here comes the fun.
|
|
// strtod is locale dependent
|
|
// istringstream will try to parse scientific notation, so things like 3em will be tried to be parsed as 3e<exponent> and consequently fail
|
|
// thus, per stackoverflow recommendation, parse the float into a separate string and then let that be parsed
|
|
std::stringstream floatss;
|
|
floatss.imbue(c_locale);
|
|
if(reader.peek() == '+' || reader.peek() == '-') floatss.put(reader.get());
|
|
reader >> std::ws;
|
|
while(std::isdigit(reader.peek()) || reader.peek() == '.') floatss.put(reader.get());
|
|
floatss >> value;
|
|
reader >> std::ws;
|
|
|
|
if (reader.peek() == '%')
|
|
{
|
|
relativeValue += value;
|
|
reader.get();
|
|
}
|
|
else if (reader.get() == 'e' && reader.get() == 'm')
|
|
{
|
|
absoluteValue += value;
|
|
}
|
|
else // error, abort! (readere is not in a clean state anyway)
|
|
{
|
|
LogF(R"(Warning: Could not parse menu format string "%s"!)", property.getStr()->GetCStr());
|
|
return;
|
|
}
|
|
|
|
reader.peek(); // get eof bit to be set
|
|
}
|
|
props[relative].SetFloat(relativeValue / 100.0f, tag);
|
|
props[absolute].SetFloat(absoluteValue, tag);
|
|
}
|
|
|
|
// for saving
|
|
C4Value C4ScriptGuiWindow::PositionToC4Value(C4ScriptGuiWindowPropertyName::type relativeName, C4ScriptGuiWindowPropertyName::type absoluteName)
|
|
{
|
|
// Go through all tags of the position attributes and save.
|
|
// Note that the tags for both the relative and the absolute attribute are always the same.
|
|
C4ScriptGuiWindowProperty &relative = props[relativeName];
|
|
C4ScriptGuiWindowProperty &absolute = props[absoluteName];
|
|
|
|
C4PropList *proplist = nullptr;
|
|
const bool onlyStdTag = relative.taggedProperties.size() == 1;
|
|
for (auto & taggedProperty : relative.taggedProperties)
|
|
{
|
|
C4String *tag = taggedProperty.first;
|
|
StdStrBuf buf;
|
|
buf.Format("%f%%%+fem", 100.0f * taggedProperty.second.f, absolute.taggedProperties[tag].f);
|
|
C4String *propString = Strings.RegString(buf);
|
|
|
|
if (onlyStdTag)
|
|
return C4Value(propString);
|
|
else
|
|
{
|
|
if (proplist == nullptr)
|
|
proplist = C4PropList::New();
|
|
proplist->SetPropertyByS(tag, C4Value(propString));
|
|
}
|
|
}
|
|
return C4Value(proplist);
|
|
}
|
|
|
|
void C4ScriptGuiWindow::Denumerate(C4ValueNumbers *numbers)
|
|
{
|
|
assert(IsRoot());
|
|
if (id == 0)
|
|
{
|
|
// nothing to do, note that the id is abused for the id in the enumeration
|
|
return;
|
|
}
|
|
C4Value value = numbers->GetValue(id);
|
|
id = 0;
|
|
CreateFromPropList(value.getPropList(), false, false, true);
|
|
|
|
for (C4GUI::Element * element : *this)
|
|
{
|
|
C4ScriptGuiWindow *mainWindow = static_cast<C4ScriptGuiWindow*>(element);
|
|
mainWindow->RequestLayoutUpdate();
|
|
}
|
|
}
|
|
|
|
const C4Value C4ScriptGuiWindow::ToC4Value()
|
|
{
|
|
C4PropList *proplist = C4PropList::New();
|
|
|
|
// it is necessary that this list contains all of the properties which can also be set somehow
|
|
// if you add something, don't forget to also add the real serialization to the loop below
|
|
int32_t toSave[] =
|
|
{
|
|
P_Left,
|
|
P_Top,
|
|
P_Right,
|
|
P_Bottom,
|
|
P_Margin,
|
|
P_BackgroundColor,
|
|
P_Decoration,
|
|
P_Symbol,
|
|
P_Target,
|
|
P_Text,
|
|
P_ID,
|
|
P_OnClick,
|
|
P_OnMouseIn,
|
|
P_OnMouseOut,
|
|
P_OnClose,
|
|
P_Style,
|
|
P_Mode,
|
|
P_Priority,
|
|
P_Player,
|
|
P_Tooltip
|
|
};
|
|
|
|
const int32_t entryCount = sizeof(toSave) / sizeof(int32_t);
|
|
|
|
for (int prop : toSave)
|
|
{
|
|
C4Value val;
|
|
|
|
switch (prop)
|
|
{
|
|
case P_Left:
|
|
case P_Top:
|
|
case P_Right:
|
|
case P_Bottom:
|
|
{
|
|
#define PROPERTY_TUPLE(p, prop1, prop2) if (prop == p) { val = PositionToC4Value(prop1, prop2); }
|
|
PROPERTY_TUPLE(P_Left, C4ScriptGuiWindowPropertyName::relLeft, C4ScriptGuiWindowPropertyName::left);
|
|
PROPERTY_TUPLE(P_Top, C4ScriptGuiWindowPropertyName::relTop, C4ScriptGuiWindowPropertyName::top);
|
|
PROPERTY_TUPLE(P_Right, C4ScriptGuiWindowPropertyName::relRight, C4ScriptGuiWindowPropertyName::right);
|
|
PROPERTY_TUPLE(P_Bottom, C4ScriptGuiWindowPropertyName::relBottom, C4ScriptGuiWindowPropertyName::bottom);
|
|
#undef PROPERTY_TUPLE
|
|
break;
|
|
}
|
|
case P_Margin: val = MarginsToC4Value(); break;
|
|
case P_BackgroundColor: val = props[C4ScriptGuiWindowPropertyName::backgroundColor].ToC4Value(); break;
|
|
case P_Decoration: val = props[C4ScriptGuiWindowPropertyName::frameDecoration].ToC4Value(); break;
|
|
case P_Symbol:
|
|
// either object or def
|
|
val = props[C4ScriptGuiWindowPropertyName::symbolObject].ToC4Value();
|
|
if (val == C4Value()) // is nil?
|
|
val = props[C4ScriptGuiWindowPropertyName::symbolDef].ToC4Value();
|
|
break;
|
|
case P_Target: val = C4Value(target); break;
|
|
case P_Text: val = props[C4ScriptGuiWindowPropertyName::text].ToC4Value(); break;
|
|
case P_GraphicsName: val = props[C4ScriptGuiWindowPropertyName::symbolGraphicsName].ToC4Value(); break;
|
|
case P_Tooltip: val = props[C4ScriptGuiWindowPropertyName::tooltip].ToC4Value(); break;
|
|
case P_ID: val = C4Value(id); break;
|
|
case P_OnClick: val = props[C4ScriptGuiWindowPropertyName::onClickAction].ToC4Value(); break;
|
|
case P_OnMouseIn: val = props[C4ScriptGuiWindowPropertyName::onMouseInAction].ToC4Value(); break;
|
|
case P_OnMouseOut: val = props[C4ScriptGuiWindowPropertyName::onMouseOutAction].ToC4Value(); break;
|
|
case P_OnClose: val = props[C4ScriptGuiWindowPropertyName::onCloseAction].ToC4Value(); break;
|
|
case P_Style: val = props[C4ScriptGuiWindowPropertyName::style].ToC4Value(); break;
|
|
case P_Mode: val = C4Value(int32_t(currentMouseState)); break;
|
|
case P_Priority: val = props[C4ScriptGuiWindowPropertyName::priority].ToC4Value(); break;
|
|
case P_Player: val = props[C4ScriptGuiWindowPropertyName::player].ToC4Value(); break;
|
|
|
|
default:
|
|
assert(false);
|
|
break;
|
|
}
|
|
|
|
// don't save "nil" values
|
|
if (val == C4Value()) continue;
|
|
|
|
proplist->SetProperty(C4PropertyName(prop), val);
|
|
}
|
|
|
|
// save children now, construct new names for them if necessary
|
|
int32_t childIndex = 0;
|
|
for (C4GUI::Element * element : *this)
|
|
{
|
|
C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
|
|
C4Value val = child->ToC4Value();
|
|
C4String *childName = child->name;
|
|
if (!childName)
|
|
{
|
|
StdStrBuf childNameBuf;
|
|
childNameBuf.Format("_child_%03d", ++childIndex);
|
|
childName = Strings.RegString(childNameBuf);
|
|
}
|
|
proplist->SetPropertyByS(childName, val);
|
|
}
|
|
|
|
return C4Value(proplist);
|
|
}
|
|
|
|
bool C4ScriptGuiWindow::CreateFromPropList(C4PropList *proplist, bool resetStdTag, bool isUpdate, bool isLoading)
|
|
{
|
|
if (!proplist) return false;
|
|
C4ScriptGuiWindow * parent = static_cast<C4ScriptGuiWindow*>(GetParent());
|
|
assert((parent || isLoading) && "GuiWindow created from proplist without parent (fails for ID tag)");
|
|
|
|
bool layoutUpdateRequired = false; // needed for position changes etc
|
|
|
|
// Get properties from proplist and check for those, that match an allowed property to set them;
|
|
// We take ownership here. Automatically destroy the object when we're done.
|
|
std::unique_ptr<C4ValueArray> properties(proplist->GetProperties());
|
|
properties->SortStrings();
|
|
C4String *stdTag = &Strings.P[P_Std];
|
|
const int32_t propertySize = properties->GetSize();
|
|
for (int32_t i = 0; i < propertySize; ++i)
|
|
{
|
|
const C4Value &entry = properties->GetItem(i);
|
|
C4String *key = entry.getStr();
|
|
assert(key && "PropList returns non-string as key");
|
|
MenuDebugLogF("--%s\t\t(I am %d)", key->GetCStr(), id);
|
|
C4Value property;
|
|
proplist->GetPropertyByS(key, &property);
|
|
|
|
if(&Strings.P[P_Left] == key)
|
|
{
|
|
SetPositionStringProperties(property, C4ScriptGuiWindowPropertyName::relLeft, C4ScriptGuiWindowPropertyName::left, stdTag);
|
|
layoutUpdateRequired = true;
|
|
}
|
|
else if(&Strings.P[P_Top] == key)
|
|
{
|
|
SetPositionStringProperties(property, C4ScriptGuiWindowPropertyName::relTop, C4ScriptGuiWindowPropertyName::top, stdTag);
|
|
layoutUpdateRequired = true;
|
|
}
|
|
else if(&Strings.P[P_Right] == key)
|
|
{
|
|
SetPositionStringProperties(property, C4ScriptGuiWindowPropertyName::relRight, C4ScriptGuiWindowPropertyName::right, stdTag);
|
|
layoutUpdateRequired = true;
|
|
}
|
|
else if(&Strings.P[P_Bottom] == key)
|
|
{
|
|
SetPositionStringProperties(property, C4ScriptGuiWindowPropertyName::relBottom, C4ScriptGuiWindowPropertyName::bottom, stdTag);
|
|
layoutUpdateRequired = true;
|
|
}
|
|
else if (&Strings.P[P_Margin] == key)
|
|
{
|
|
SetMarginProperties(property, stdTag);
|
|
layoutUpdateRequired = true;
|
|
}
|
|
else if(&Strings.P[P_BackgroundColor] == key)
|
|
props[C4ScriptGuiWindowPropertyName::backgroundColor].Set(property, stdTag);
|
|
else if(&Strings.P[P_Target] == key)
|
|
target = property.getObj();
|
|
else if(&Strings.P[P_Symbol] == key)
|
|
{
|
|
props[C4ScriptGuiWindowPropertyName::symbolDef].Set(property, stdTag);
|
|
props[C4ScriptGuiWindowPropertyName::symbolObject].Set(property, stdTag);
|
|
}
|
|
else if(&Strings.P[P_Decoration] == key)
|
|
{
|
|
props[C4ScriptGuiWindowPropertyName::frameDecoration].Set(property, stdTag);
|
|
}
|
|
else if(&Strings.P[P_Text] == key)
|
|
{
|
|
props[C4ScriptGuiWindowPropertyName::text].Set(property, stdTag);
|
|
layoutUpdateRequired = true;
|
|
}
|
|
else if (&Strings.P[P_GraphicsName] == key)
|
|
{
|
|
props[C4ScriptGuiWindowPropertyName::symbolGraphicsName].Set(property, stdTag);
|
|
}
|
|
else if (&Strings.P[P_Tooltip] == key)
|
|
{
|
|
props[C4ScriptGuiWindowPropertyName::tooltip].Set(property, stdTag);
|
|
}
|
|
else if(&Strings.P[P_Prototype] == key)
|
|
; // do nothing
|
|
else if (&Strings.P[P_Mode] == key) // note that "Mode" is abused here for saving whether we have mouse focus
|
|
{
|
|
if (isLoading)
|
|
currentMouseState = property.getInt();
|
|
}
|
|
else if(&Strings.P[P_ID] == key)
|
|
{
|
|
// setting IDs is only valid for subwindows or when loading savegames!
|
|
if (parent && !isMainWindow)
|
|
{
|
|
if (id) // already have an ID? remove from parent
|
|
parent->ChildWithIDRemoved(this);
|
|
id = property.getInt();
|
|
if (id != 0)
|
|
parent->ChildGotID(this);
|
|
}
|
|
else
|
|
if (isLoading)
|
|
id = property.getInt();
|
|
}
|
|
else if (&Strings.P[P_OnClick] == key)
|
|
{
|
|
MenuDebugLogF("Adding new action, I am window %d with parent %d", id, static_cast<C4ScriptGuiWindow*>(parent)->id);
|
|
props[C4ScriptGuiWindowPropertyName::onClickAction].Set(property, stdTag);
|
|
}
|
|
else if(&Strings.P[P_OnMouseIn] == key)
|
|
props[C4ScriptGuiWindowPropertyName::onMouseInAction].Set(property, stdTag);
|
|
else if(&Strings.P[P_OnMouseOut] == key)
|
|
props[C4ScriptGuiWindowPropertyName::onMouseOutAction].Set(property, stdTag);
|
|
else if(&Strings.P[P_OnClose] == key)
|
|
props[C4ScriptGuiWindowPropertyName::onCloseAction].Set(property, stdTag);
|
|
else if(&Strings.P[P_Style] == key)
|
|
{
|
|
props[C4ScriptGuiWindowPropertyName::style].Set(property, stdTag);
|
|
layoutUpdateRequired = true;
|
|
}
|
|
else if(&Strings.P[P_Priority] == key)
|
|
{
|
|
props[C4ScriptGuiWindowPropertyName::priority].Set(property, stdTag);
|
|
layoutUpdateRequired = true;
|
|
// resort into parent's list
|
|
if (parent)
|
|
parent->ChildChangedPriority(this);
|
|
}
|
|
else if(&Strings.P[P_Player] == key)
|
|
props[C4ScriptGuiWindowPropertyName::player].Set(property, stdTag);
|
|
else
|
|
{
|
|
// possibly sub-window?
|
|
C4PropList *subwindow = property.getPropList();
|
|
if (subwindow)
|
|
{
|
|
// remember the name of the child; but ignore names starting with underscores
|
|
C4String *childName = nullptr;
|
|
if (key->GetCStr()[0] != '_')
|
|
childName = key;
|
|
|
|
// Do we already have a child with that name? That implies that we are updating here.
|
|
C4ScriptGuiWindow *child = GetChildByName(childName);
|
|
bool freshlyAdded = false;
|
|
|
|
// first time referencing a child with that name? Create a new one!
|
|
if (!child)
|
|
{
|
|
child = new C4ScriptGuiWindow();
|
|
if (childName != nullptr)
|
|
{
|
|
child->name = childName;
|
|
child->name->IncRef();
|
|
}
|
|
AddChild(child);
|
|
freshlyAdded = true;
|
|
}
|
|
|
|
if (!child->CreateFromPropList(subwindow, isUpdate == true, false, isLoading))
|
|
{
|
|
// Remove the child again if we just added it. However, ignore when just updating an existing child.
|
|
if (freshlyAdded)
|
|
RemoveChild(child, false);
|
|
}
|
|
else
|
|
layoutUpdateRequired = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isLoading && layoutUpdateRequired)
|
|
RequestLayoutUpdate();
|
|
|
|
if (resetStdTag || isLoading)
|
|
SetTag(stdTag);
|
|
|
|
return true;
|
|
}
|
|
|
|
void C4ScriptGuiWindow::ClearPointers(C4Object *pObj)
|
|
{
|
|
// not removing or clearing anything twice
|
|
// if this flag is set, the object will not be used after this frame (callbacks?) anyway
|
|
if (wasRemoved) return;
|
|
|
|
if (target == pObj)
|
|
{
|
|
Close();
|
|
return;
|
|
}
|
|
|
|
// all properties which have anything to do with objects need to be called from here!
|
|
props[C4ScriptGuiWindowPropertyName::symbolObject].ClearPointers(pObj);
|
|
props[C4ScriptGuiWindowPropertyName::onClickAction].ClearPointers(pObj);
|
|
props[C4ScriptGuiWindowPropertyName::onMouseInAction].ClearPointers(pObj);
|
|
props[C4ScriptGuiWindowPropertyName::onMouseOutAction].ClearPointers(pObj);
|
|
props[C4ScriptGuiWindowPropertyName::onCloseAction].ClearPointers(pObj);
|
|
|
|
for (auto iter = begin(); iter != end();)
|
|
{
|
|
C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(*iter);
|
|
// increment the iterator before (possibly) deleting the child
|
|
++iter;
|
|
child->ClearPointers(pObj);
|
|
}
|
|
}
|
|
|
|
C4ScriptGuiWindow *C4ScriptGuiWindow::AddChild(C4ScriptGuiWindow *child)
|
|
{
|
|
if (IsRoot())
|
|
{
|
|
child->SetID(GenerateMenuID());
|
|
child->isMainWindow = true;
|
|
// update all windows asap
|
|
mainWindowNeedsLayoutUpdate = true;
|
|
}
|
|
|
|
// child's priority is ususally 0 here, so just insert it in front of other windows with a priority below 0
|
|
// when the child's priority updates, the update function will be called and the child will be sorted to the correct position
|
|
ChildChangedPriority(child);
|
|
|
|
return child;
|
|
}
|
|
|
|
void C4ScriptGuiWindow::ChildChangedPriority(C4ScriptGuiWindow *child)
|
|
{
|
|
int prio = child->props[C4ScriptGuiWindowPropertyName::priority].GetInt();
|
|
C4GUI::Element * insertBefore = nullptr;
|
|
|
|
for (C4GUI::Element * element : *this)
|
|
{
|
|
C4ScriptGuiWindow * otherChild = static_cast<C4ScriptGuiWindow*>(element);
|
|
if (otherChild->props[C4ScriptGuiWindowPropertyName::priority].GetInt() <= prio) continue;
|
|
insertBefore = element;
|
|
break;
|
|
}
|
|
// if the child is already at the correct position, do nothing
|
|
assert(child != insertBefore);
|
|
// resort
|
|
// this method will take care of removing and re-adding the child
|
|
InsertElement(child, insertBefore);
|
|
}
|
|
|
|
void C4ScriptGuiWindow::ChildWithIDRemoved(C4ScriptGuiWindow *child)
|
|
{
|
|
if (IsRoot()) return;
|
|
if (!isMainWindow)
|
|
return static_cast<C4ScriptGuiWindow*>(GetParent())->ChildWithIDRemoved(child);
|
|
std::pair<std::multimap<int32_t, C4ScriptGuiWindow*>::iterator, std::multimap<int32_t, C4ScriptGuiWindow*>::iterator> range;
|
|
range = childrenIDMap.equal_range(child->GetID());
|
|
|
|
for (std::multimap<int32_t, C4ScriptGuiWindow*>::iterator iter = range.first; iter != range.second; ++iter)
|
|
{
|
|
if (iter->second != child) continue;
|
|
childrenIDMap.erase(iter);
|
|
MenuDebugLogF("child-map-size: %d, remove %d [I am %d]", childrenIDMap.size(), child->GetID(), id);
|
|
return;
|
|
}
|
|
}
|
|
|
|
void C4ScriptGuiWindow::ChildGotID(C4ScriptGuiWindow *child)
|
|
{
|
|
assert(!IsRoot() && "ChildGotID called on window root, should not propagate over main windows!");
|
|
if (!isMainWindow)
|
|
return static_cast<C4ScriptGuiWindow*>(GetParent())->ChildGotID(child);
|
|
childrenIDMap.insert(std::make_pair(child->GetID(), child));
|
|
MenuDebugLogF("child+map+size: %d, added %d [I am %d]", childrenIDMap.size(), child->GetID(), id);
|
|
}
|
|
|
|
C4ScriptGuiWindow *C4ScriptGuiWindow::GetChildByID(int32_t childID)
|
|
{
|
|
for (Element * element : *this)
|
|
{
|
|
C4ScriptGuiWindow * child = static_cast<C4ScriptGuiWindow*>(element);
|
|
if (child->id != childID) continue;
|
|
return child;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
C4ScriptGuiWindow *C4ScriptGuiWindow::GetChildByName(C4String *childName)
|
|
{
|
|
// invalid child names never match
|
|
if (childName == nullptr) return nullptr;
|
|
|
|
for (Element * element : *this)
|
|
{
|
|
C4ScriptGuiWindow * child = static_cast<C4ScriptGuiWindow*>(element);
|
|
// every C4String is unique, so we can compare pointers here
|
|
if (child->name != childName) continue;
|
|
return child;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
C4ScriptGuiWindow *C4ScriptGuiWindow::GetSubWindow(int32_t childID, C4Object *childTarget)
|
|
{
|
|
std::pair<std::multimap<int32_t, C4ScriptGuiWindow*>::iterator, std::multimap<int32_t, C4ScriptGuiWindow*>::iterator> range;
|
|
range = childrenIDMap.equal_range(childID);
|
|
|
|
for (std::multimap<int32_t, C4ScriptGuiWindow*>::iterator iter = range.first; iter != range.second; ++iter)
|
|
{
|
|
C4ScriptGuiWindow *subwindow = iter->second;
|
|
if (subwindow->GetTarget() != childTarget) continue;
|
|
return subwindow;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void C4ScriptGuiWindow::RemoveChild(C4ScriptGuiWindow *child, bool close, bool all)
|
|
{
|
|
// do a layout update asap
|
|
if (!all && !IsRoot())
|
|
RequestLayoutUpdate();
|
|
|
|
if (child)
|
|
{
|
|
child->wasRemoved = true;
|
|
if (close) child->Close();
|
|
if (child->GetID() != 0)
|
|
ChildWithIDRemoved(child);
|
|
RemoveElement(static_cast<C4GUI::Element*>(child));
|
|
// RemoveElement does NOT delete the child itself.
|
|
delete child;
|
|
}
|
|
else if (close) // close all children
|
|
{
|
|
assert(all);
|
|
for (Element * element : *this)
|
|
{
|
|
C4ScriptGuiWindow * child = static_cast<C4ScriptGuiWindow*>(element);
|
|
child->wasRemoved = true;
|
|
child->Close();
|
|
if (child->GetID() != 0)
|
|
ChildWithIDRemoved(child);
|
|
}
|
|
}
|
|
|
|
if (all)
|
|
C4GUI::ScrollWindow::ClearChildren();
|
|
}
|
|
|
|
void C4ScriptGuiWindow::ClearChildren(bool close)
|
|
{
|
|
RemoveChild(nullptr, close, true);
|
|
}
|
|
|
|
void C4ScriptGuiWindow::Close()
|
|
{
|
|
// first, close all children and dispose of them properly
|
|
ClearChildren(true);
|
|
|
|
if (!closeActionWasExecuted)
|
|
{
|
|
closeActionWasExecuted = true;
|
|
|
|
// make call to target object if applicable
|
|
C4ScriptGuiWindowAction *action = props[C4ScriptGuiWindowPropertyName::onCloseAction].GetAction();
|
|
// only calls are valid actions for OnClose
|
|
if (action && action->action == C4ScriptGuiWindowActionID::Call)
|
|
{
|
|
// close is always syncronized (script call/object removal) and thus the action can be executed immediately
|
|
// (otherwise the GUI&action would have been removed anyway..)
|
|
action->ExecuteCommand(action->id, this, NO_OWNER);
|
|
}
|
|
}
|
|
|
|
if (!wasRemoved)
|
|
{
|
|
assert(GetParent() && "Close()ing GUIWindow without parent");
|
|
static_cast<C4ScriptGuiWindow*>(GetParent())->RemoveChild(this);
|
|
}
|
|
}
|
|
|
|
void C4ScriptGuiWindow::EnableScrollBar(bool enable, float childrenHeight)
|
|
{
|
|
const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
|
|
|
|
if (style & C4ScriptGuiWindowStyleFlag::FitChildren)
|
|
{
|
|
float height = float(rcBounds.Hgt)
|
|
- Em2Pix(props[C4ScriptGuiWindowPropertyName::topMargin].GetFloat())
|
|
- Em2Pix(props[C4ScriptGuiWindowPropertyName::bottomMargin].GetFloat());
|
|
float adjustment = childrenHeight - height;
|
|
props[C4ScriptGuiWindowPropertyName::bottom].current->f += Pix2Em(adjustment);
|
|
assert(!std::isnan(props[C4ScriptGuiWindowPropertyName::bottom].current->f));
|
|
// instantly pseudo-update the sizes in case of multiple refreshs before the next draw
|
|
rcBounds.Hgt += adjustment;
|
|
// parents that are somehow affected by their children will need to refresh their layout
|
|
if (adjustment != 0.0)
|
|
RequestLayoutUpdate();
|
|
return;
|
|
}
|
|
|
|
if (style & C4ScriptGuiWindowStyleFlag::NoCrop) return;
|
|
|
|
C4GUI::ScrollWindow::SetScrollBarEnabled(enable, true);
|
|
}
|
|
|
|
|
|
float C4ScriptGuiWindow::CalculateRelativeSize(float parentWidthOrHeight, C4ScriptGuiWindowPropertyName::type absoluteProperty, C4ScriptGuiWindowPropertyName::type relativeProperty)
|
|
{
|
|
const float widthOrHeight = Em2Pix(props[absoluteProperty].GetFloat())
|
|
+ float(parentWidthOrHeight) * props[relativeProperty].GetFloat();
|
|
return widthOrHeight;
|
|
}
|
|
|
|
|
|
void C4ScriptGuiWindow::UpdateLayoutGrid()
|
|
{
|
|
const int32_t &width = rcBounds.Wdt;
|
|
const int32_t &height = rcBounds.Hgt;
|
|
|
|
const int32_t borderX(0), borderY(0);
|
|
int32_t currentX = borderX;
|
|
int32_t currentY = borderY;
|
|
int32_t lowestChildRelY = 0;
|
|
int32_t maxChildHeight = 0;
|
|
|
|
for (C4GUI::Element * element : *this)
|
|
{
|
|
C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
|
|
// calculate the space the child needs, correctly respecting the margins
|
|
using namespace C4ScriptGuiWindowPropertyName;
|
|
const float childLeftMargin = child->CalculateRelativeSize(width, leftMargin, relLeftMargin);
|
|
const float childTopMargin = child->CalculateRelativeSize(height, topMargin, relTopMargin);
|
|
const float childRightMargin = child->CalculateRelativeSize(width, rightMargin, relRightMargin);
|
|
const float childBottomMargin = child->CalculateRelativeSize(height, bottomMargin, relBottomMargin);
|
|
|
|
const float childWdtF = float(child->rcBounds.Wdt) + childLeftMargin + childRightMargin;
|
|
const float childHgtF = float(child->rcBounds.Hgt) + childTopMargin + childBottomMargin;
|
|
|
|
auto doLineBreak = [&]()
|
|
{
|
|
currentX = borderX;
|
|
currentY += maxChildHeight + borderY;
|
|
maxChildHeight = 0;
|
|
};
|
|
|
|
// do all the rounding after the calculations
|
|
const auto childWdt = (int32_t)(childWdtF + 0.5f);
|
|
const auto childHgt = (int32_t)(childHgtF + 0.5f);
|
|
|
|
// Check if the child even fits in the remainder of the row
|
|
const bool fitsInRow = (width - currentX) >= childWdt;
|
|
if (!fitsInRow) doLineBreak();
|
|
|
|
// remember the highest child to make sure rows don't overlap
|
|
if (!maxChildHeight || (childHgt > maxChildHeight))
|
|
{
|
|
maxChildHeight = childHgt;
|
|
lowestChildRelY = currentY + childHgt;
|
|
}
|
|
child->rcBounds.x = currentX + static_cast<int32_t>(childLeftMargin);
|
|
child->rcBounds.y = currentY + static_cast<int32_t>(childTopMargin);
|
|
|
|
currentX += childWdt + borderX;
|
|
}
|
|
|
|
// do we need a scroll bar?
|
|
EnableScrollBar(lowestChildRelY > height, lowestChildRelY);
|
|
}
|
|
|
|
// Similar to the grid layout but tries to fill spaces more thoroughly.
|
|
// It's slower and might reorder items.
|
|
void C4ScriptGuiWindow::UpdateLayoutTightGrid()
|
|
{
|
|
const int32_t &width = rcBounds.Wdt;
|
|
const int32_t &height = rcBounds.Hgt;
|
|
const int32_t borderX(0), borderY(0);
|
|
int32_t lowestChildRelY = 0;
|
|
|
|
std::list<C4ScriptGuiWindow*> alreadyPlacedChildren;
|
|
|
|
for (C4GUI::Element * element : *this)
|
|
{
|
|
C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
|
|
// calculate the space the child needs, correctly respecting the margins
|
|
using namespace C4ScriptGuiWindowPropertyName;
|
|
const float childLeftMargin = child->CalculateRelativeSize(width, leftMargin, relLeftMargin);
|
|
const float childTopMargin = child->CalculateRelativeSize(height, topMargin, relTopMargin);
|
|
const float childRightMargin = child->CalculateRelativeSize(width, rightMargin, relRightMargin);
|
|
const float childBottomMargin = child->CalculateRelativeSize(height, bottomMargin, relBottomMargin);
|
|
|
|
const float childWdtF = float(child->rcBounds.Wdt) + childLeftMargin + childRightMargin;
|
|
const float childHgtF = float(child->rcBounds.Hgt) + childTopMargin + childBottomMargin;
|
|
|
|
// do all the rounding after the calculations
|
|
const auto childWdt = (int32_t)(childWdtF + 0.5f);
|
|
const auto childHgt = (int32_t)(childHgtF + 0.5f);
|
|
|
|
// Look for a free spot.
|
|
int32_t currentX = borderX;
|
|
int32_t currentY = borderY;
|
|
|
|
bool hadOverlap = false;
|
|
int overlapRepeats = 0;
|
|
do
|
|
{
|
|
auto overlapsWithOther = [¤tX, ¤tY, &childWdt, &childHgt](C4ScriptGuiWindow *other)
|
|
{
|
|
if (currentX + childWdt <= other->rcBounds.x) return false;
|
|
if (currentY + childHgt <= other->rcBounds.y) return false;
|
|
if (currentX >= other->rcBounds.GetRight()) return false;
|
|
if (currentY >= other->rcBounds.GetBottom()) return false;
|
|
return true;
|
|
};
|
|
|
|
int32_t currentMinY = 0;
|
|
hadOverlap = false;
|
|
for (auto &other : alreadyPlacedChildren)
|
|
{
|
|
// Check if the other element is not yet above the new child.
|
|
if ((other->rcBounds.GetBottom() > currentY) && other->rcBounds.Hgt > 0)
|
|
{
|
|
if (currentMinY == 0 || (other->rcBounds.GetBottom() < currentMinY))
|
|
currentMinY = other->rcBounds.GetBottom();
|
|
}
|
|
// If overlapping, we must advance.
|
|
if (overlapsWithOther(other))
|
|
{
|
|
hadOverlap = true;
|
|
currentX = other->rcBounds.GetRight();
|
|
// Break line if the element doesn't fit anymore.
|
|
if (currentX + childWdt > width)
|
|
{
|
|
currentX = borderX;
|
|
// Start forcing change once we start repeating the check. Otherwise, there might
|
|
// be a composition of children that lead to infinite loop. The worst-case number
|
|
// of sensible checks might O(N^2) be with a really unfortunate children list.
|
|
const int32_t forcedMinimalChange = (overlapRepeats > alreadyPlacedChildren.size()) ? 1 : 0;
|
|
currentY = std::max(currentY + forcedMinimalChange, currentMinY);
|
|
}
|
|
}
|
|
}
|
|
overlapRepeats += 1;
|
|
} while (hadOverlap);
|
|
|
|
alreadyPlacedChildren.push_back(child);
|
|
|
|
lowestChildRelY = std::max(lowestChildRelY, currentY + childHgt);
|
|
child->rcBounds.x = currentX + static_cast<int32_t>(childLeftMargin);
|
|
child->rcBounds.y = currentY + static_cast<int32_t>(childTopMargin);
|
|
}
|
|
|
|
// do we need a scroll bar?
|
|
EnableScrollBar(lowestChildRelY > height, lowestChildRelY);
|
|
}
|
|
|
|
void C4ScriptGuiWindow::UpdateLayoutVertical()
|
|
{
|
|
const int32_t borderY(0);
|
|
int32_t currentY = borderY;
|
|
|
|
for (C4GUI::Element * element : *this)
|
|
{
|
|
C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
|
|
|
|
// Do the calculations in floats first to not lose accuracy.
|
|
// Take the height of the child and then add the margins.
|
|
using namespace C4ScriptGuiWindowPropertyName;
|
|
const float childTopMargin = child->CalculateRelativeSize(rcBounds.Hgt, topMargin, relTopMargin);
|
|
const float childBottomMargin = child->CalculateRelativeSize(rcBounds.Hgt, bottomMargin, relBottomMargin);
|
|
|
|
const float childHgtF = float(child->rcBounds.Hgt) + childTopMargin + childBottomMargin;
|
|
const int32_t childHgt = (int32_t)(childHgtF + 0.5f);
|
|
|
|
child->rcBounds.y = currentY + childTopMargin;
|
|
currentY += childHgt + borderY;
|
|
}
|
|
|
|
// do we need a scroll bar?
|
|
EnableScrollBar(currentY > rcBounds.Hgt, currentY);
|
|
}
|
|
|
|
bool C4ScriptGuiWindow::DrawChildren(C4TargetFacet &cgo, int32_t player, int32_t withMultipleFlag, C4Rect *currentClippingRect)
|
|
{
|
|
// remember old target rectangle and adjust
|
|
float oldTargetX = cgo.TargetX;
|
|
float oldTargetY = cgo.TargetY;
|
|
C4Rect myClippingRect;
|
|
if (IsRoot())
|
|
{
|
|
cgo.TargetX = 0;
|
|
cgo.TargetY = 0;
|
|
pDraw->StorePrimaryClipper();
|
|
// default: full screen clipper
|
|
myClippingRect = C4Rect(0, 0, cgo.Wdt * cgo.Zoom, cgo.Hgt * cgo.Zoom);
|
|
currentClippingRect = &myClippingRect;
|
|
}
|
|
|
|
// if ANY PARENT has scroll bar, then adjust clipper
|
|
int32_t clipX1(0), clipX2(0), clipY1(0), clipY2(0);
|
|
bool clipping = GetClippingRect(clipX1, clipY1, clipX2, clipY2);
|
|
|
|
const int32_t targetClipX1 = cgo.X + cgo.TargetX + clipX1;
|
|
const int32_t targetClipY1 = cgo.Y + cgo.TargetY + clipY1;
|
|
const int32_t targetClipX2 = cgo.X + cgo.TargetX + clipX2;
|
|
const int32_t targetClipY2 = cgo.Y + cgo.TargetY + clipY2;
|
|
|
|
if (clipping)
|
|
{
|
|
// Take either the parent rectangle or restrict it additionally by the child's geometry.
|
|
myClippingRect = C4Rect(
|
|
std::max(currentClippingRect->x, targetClipX1),
|
|
std::max(currentClippingRect->y, targetClipY1),
|
|
std::min(currentClippingRect->Wdt, targetClipX2),
|
|
std::min(currentClippingRect->Hgt, targetClipY2));
|
|
currentClippingRect = &myClippingRect;
|
|
}
|
|
|
|
if (withMultipleFlag != 1)
|
|
{
|
|
cgo.TargetX += rcBounds.x;
|
|
cgo.TargetY += rcBounds.y - iScrollY;
|
|
}
|
|
else
|
|
{
|
|
assert(IsRoot());
|
|
assert(withMultipleFlag == 1);
|
|
}
|
|
|
|
|
|
// note that withMultipleFlag only plays a roll for the root-menu
|
|
bool oneDrawn = false; // was at least one child drawn?
|
|
//for (auto iter = rbegin(); iter != rend(); ++iter)
|
|
for (auto element : *this)
|
|
{
|
|
C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
|
|
|
|
if (withMultipleFlag != -1)
|
|
{
|
|
const int32_t &style = child->props[C4ScriptGuiWindowPropertyName::style].GetInt();
|
|
if ((withMultipleFlag == 0) && (style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
|
|
if ((withMultipleFlag == 1) && !(style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
|
|
}
|
|
|
|
pDraw->SetPrimaryClipper(currentClippingRect->x, currentClippingRect->y, currentClippingRect->Wdt, currentClippingRect->Hgt);
|
|
|
|
if (child->Draw(cgo, player, currentClippingRect))
|
|
oneDrawn = true;
|
|
// draw only one window when drawing non-Multiple windows
|
|
if (oneDrawn && (withMultipleFlag == 0)) break;
|
|
}
|
|
|
|
// Scrolling obviously does not affect the scroll bar.
|
|
cgo.TargetY += iScrollY;
|
|
// The scroll bar does not correct for the cgo offset (i.e. the upper board).
|
|
cgo.TargetX += cgo.X;
|
|
cgo.TargetY += cgo.Y;
|
|
|
|
if (pScrollBar->IsVisible())
|
|
pScrollBar->DrawElement(cgo);
|
|
|
|
if (IsRoot())
|
|
{
|
|
pDraw->RestorePrimaryClipper();
|
|
}
|
|
|
|
// restore target rectangle
|
|
cgo.TargetX = oldTargetX;
|
|
cgo.TargetY = oldTargetY;
|
|
return oneDrawn;
|
|
}
|
|
|
|
void C4ScriptGuiWindow::RequestLayoutUpdate()
|
|
{
|
|
// directly requested on the root window?
|
|
// That usually comes from another part of the engine (f.e. C4Viewport::RecalculateViewports) or from a multiple-window child
|
|
if (!GetParent())
|
|
{
|
|
mainWindowNeedsLayoutUpdate = true;
|
|
return;
|
|
}
|
|
|
|
// are we a direct child of the root?
|
|
if (isMainWindow)
|
|
{
|
|
const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
|
|
|
|
if (!(style & C4ScriptGuiWindowStyleFlag::Multiple)) // are we a simple centered window?
|
|
{
|
|
mainWindowNeedsLayoutUpdate = true;
|
|
return;
|
|
}
|
|
else // we are one of the multiple windows.. the root better do a full refresh
|
|
{}
|
|
}
|
|
// propagate to parent window
|
|
static_cast<C4ScriptGuiWindow*>(GetParent())->RequestLayoutUpdate();
|
|
}
|
|
|
|
bool C4ScriptGuiWindow::UpdateChildLayout(C4TargetFacet &cgo, float parentWidth, float parentHeight)
|
|
{
|
|
for (Element * element : *this)
|
|
{
|
|
C4ScriptGuiWindow *window = static_cast<C4ScriptGuiWindow*>(element);
|
|
window->UpdateLayout(cgo, parentWidth, parentHeight);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool C4ScriptGuiWindow::UpdateLayout(C4TargetFacet &cgo)
|
|
{
|
|
assert(IsRoot()); // we are root
|
|
|
|
// assume I am the root and use the a special rectangle in the viewport for drawing
|
|
const float fullWidth = cgo.Wdt * cgo.Zoom - cgo.X;
|
|
const float fullHeight = cgo.Hgt * cgo.Zoom - cgo.Y;
|
|
|
|
// golden ratio defined default size!
|
|
const float &targetWidthEm = C4ScriptGuiWindow::standardWidth;
|
|
const float &targetHeightEm = C4ScriptGuiWindow::standardHeight;
|
|
|
|
// adjust by viewport size
|
|
const float minMarginPx = 50.0f;
|
|
const float targetWidthPx = std::min(Em2Pix(targetWidthEm), fullWidth - 2.0f * minMarginPx);
|
|
const float targetHeightPx = std::min(Em2Pix(targetHeightEm), fullHeight - 2.0f * minMarginPx);
|
|
|
|
// calculate margins to center the window
|
|
const float marginLeftRight = (fullWidth - targetWidthPx) / 2.0f;
|
|
const float marginTopBottom = (fullHeight- targetHeightPx) / 2.0f;
|
|
|
|
// we can only position the window by adjusting left/right/top/bottom
|
|
const float &left = marginLeftRight;
|
|
const float right = -marginLeftRight;
|
|
const float &top = marginTopBottom;
|
|
const float bottom = -marginTopBottom;
|
|
|
|
// actual size, calculated from borders
|
|
const float wdt = fullWidth - left + right;
|
|
const float hgt = fullHeight - top + bottom;
|
|
|
|
const bool needUpdate = mainWindowNeedsLayoutUpdate || (rcBounds.Wdt != int32_t(wdt)) || (rcBounds.Hgt != int32_t(hgt));
|
|
|
|
if (needUpdate)
|
|
{
|
|
mainWindowNeedsLayoutUpdate = false;
|
|
|
|
// these are the coordinates for the centered non-multiple windows
|
|
rcBounds.x = static_cast<int>(left);
|
|
rcBounds.y = static_cast<int>(top);
|
|
rcBounds.Wdt = wdt;
|
|
rcBounds.Hgt = hgt;
|
|
|
|
// first update all multiple windows (that can cover the whole screen)
|
|
for (Element * element : *this)
|
|
{
|
|
C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
|
|
const int32_t &style = child->props[C4ScriptGuiWindowPropertyName::style].GetInt();
|
|
if (!(style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
|
|
child->UpdateLayout(cgo, fullWidth, fullHeight);
|
|
}
|
|
// then update all "main" windows in the center of the screen
|
|
// todo: adjust the size of the main window based on the border-windows drawn before
|
|
for (Element * element : *this)
|
|
{
|
|
C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
|
|
const int32_t &style = child->props[C4ScriptGuiWindowPropertyName::style].GetInt();
|
|
if ((style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
|
|
child->UpdateLayout(cgo, wdt, hgt);
|
|
}
|
|
|
|
pScrollBar->SetVisibility(false);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool C4ScriptGuiWindow::UpdateLayout(C4TargetFacet &cgo, float parentWidth, float parentHeight)
|
|
{
|
|
// fetch style
|
|
const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
|
|
// fetch current position as shortcut for overview
|
|
const float &left = props[C4ScriptGuiWindowPropertyName::left].GetFloat();
|
|
const float &right = props[C4ScriptGuiWindowPropertyName::right].GetFloat();
|
|
const float &top = props[C4ScriptGuiWindowPropertyName::top].GetFloat();
|
|
const float &bottom = props[C4ScriptGuiWindowPropertyName::bottom].GetFloat();
|
|
|
|
const float &relLeft = props[C4ScriptGuiWindowPropertyName::relLeft].GetFloat();
|
|
const float &relRight = props[C4ScriptGuiWindowPropertyName::relRight].GetFloat();
|
|
const float &relTop = props[C4ScriptGuiWindowPropertyName::relTop].GetFloat();
|
|
const float &relBottom = props[C4ScriptGuiWindowPropertyName::relBottom].GetFloat();
|
|
|
|
// same for margins
|
|
const float &leftMargin = props[C4ScriptGuiWindowPropertyName::leftMargin].GetFloat();
|
|
const float &rightMargin = props[C4ScriptGuiWindowPropertyName::rightMargin].GetFloat();
|
|
const float &topMargin = props[C4ScriptGuiWindowPropertyName::topMargin].GetFloat();
|
|
const float &bottomMargin = props[C4ScriptGuiWindowPropertyName::bottomMargin].GetFloat();
|
|
|
|
const float &relLeftMargin = props[C4ScriptGuiWindowPropertyName::relLeftMargin].GetFloat();
|
|
const float &relRightMargin = props[C4ScriptGuiWindowPropertyName::relRightMargin].GetFloat();
|
|
const float &relTopMargin = props[C4ScriptGuiWindowPropertyName::relTopMargin].GetFloat();
|
|
const float &relBottomMargin = props[C4ScriptGuiWindowPropertyName::relBottomMargin].GetFloat();
|
|
|
|
// calculate drawing rectangle
|
|
float leftDrawX = relLeft * parentWidth + Em2Pix(left) + (Em2Pix(leftMargin) + relLeftMargin * parentWidth);
|
|
float rightDrawX = relRight * parentWidth + Em2Pix(right) - (Em2Pix(rightMargin) + relRightMargin * parentWidth);
|
|
float topDrawY = relTop * parentHeight + Em2Pix(top) + (Em2Pix(topMargin) + relTopMargin * parentHeight);
|
|
float bottomDrawY = relBottom * parentHeight + Em2Pix(bottom) - (Em2Pix(bottomMargin) + relBottomMargin * parentHeight);
|
|
float width = rightDrawX - leftDrawX;
|
|
float height = bottomDrawY - topDrawY;
|
|
|
|
rcBounds.x = leftDrawX;
|
|
rcBounds.y = topDrawY;
|
|
rcBounds.Wdt = width;
|
|
rcBounds.Hgt = height;
|
|
|
|
// If this window contains text, we auto-fit to the text height;
|
|
// but we only break text when the window /would/ crop it otherwise.
|
|
StdCopyStrBuf *strBuf = props[C4ScriptGuiWindowPropertyName::text].GetStrBuf();
|
|
int minRequiredTextHeight = 0;
|
|
if (strBuf && !(style & C4ScriptGuiWindowStyleFlag::NoCrop))
|
|
{
|
|
StdStrBuf sText;
|
|
const int32_t rawTextHeight = ::GraphicsResource.FontRegular.BreakMessage(strBuf->getData(), rcBounds.Wdt, &sText, true);
|
|
// enable auto scroll
|
|
if (rawTextHeight - 1 > rcBounds.Hgt)
|
|
{
|
|
// If we need to scroll, we will also have to add a scrollbar that takes up some width.
|
|
// Recalculate the actual height, taking into account the scrollbar.
|
|
// This is not done in the calculation earlier, because then e.g. a 2x1em field could not contain two letters
|
|
// but would immediately add a linebreak.
|
|
// In the case that this window auto-resizes (FitChildren), the small additional margin to the bottom should not matter much.
|
|
const int32_t actualTextHeight = ::GraphicsResource.FontRegular.BreakMessage(strBuf->getData(), rcBounds.Wdt - pScrollBar->rcBounds.Wdt, &sText, true);
|
|
minRequiredTextHeight = actualTextHeight;
|
|
}
|
|
else
|
|
{
|
|
// Otherwise, still set the minimum size to the text height (without scrollbar).
|
|
// This is necessary so that e.g. Style::FitChildren works properly with pure text windows.
|
|
minRequiredTextHeight = rawTextHeight;
|
|
}
|
|
}
|
|
|
|
UpdateChildLayout(cgo, width, height);
|
|
|
|
// update scroll bar
|
|
// C4GUI::ScrollWindow::UpdateOwnPos();
|
|
|
|
// special layout selected?
|
|
if (style & C4ScriptGuiWindowStyleFlag::GridLayout)
|
|
UpdateLayoutGrid();
|
|
else if (style & C4ScriptGuiWindowStyleFlag::TightGridLayout)
|
|
UpdateLayoutTightGrid();
|
|
else if (style & C4ScriptGuiWindowStyleFlag::VerticalLayout)
|
|
UpdateLayoutVertical();
|
|
|
|
// check if we need a scroll-bar
|
|
int32_t topMostChild = 0;
|
|
int32_t bottomMostChild = minRequiredTextHeight;
|
|
for (Element * element : *this)
|
|
{
|
|
C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
|
|
const int32_t &childTop = child->rcBounds.y;
|
|
const int32_t childBottom = childTop + child->rcBounds.Hgt;
|
|
if (childTop < topMostChild) topMostChild = childTop;
|
|
if (childBottom > bottomMostChild) bottomMostChild = childBottom;
|
|
}
|
|
// do we need to adjust our size to fit the child windows?
|
|
if (style & C4ScriptGuiWindowStyleFlag::FitChildren)
|
|
{
|
|
rcBounds.Hgt = bottomMostChild;
|
|
}
|
|
// update scroll rectangle:
|
|
// (subtract one against rounding errors)
|
|
bottomMostChild = std::max(bottomMostChild, rcBounds.Hgt);
|
|
iClientHeight = bottomMostChild - topMostChild - 1;
|
|
C4GUI::ScrollWindow::Update();
|
|
|
|
pScrollBar->rcBounds.Wdt = C4GUI_ScrollBarWdt;
|
|
pScrollBar->rcBounds.x = rcBounds.Wdt - pScrollBar->rcBounds.Wdt;
|
|
pScrollBar->rcBounds.y = 0;
|
|
pScrollBar->rcBounds.Hgt = rcBounds.Hgt;
|
|
pScrollBar->Update();
|
|
|
|
// never show scrollbar on non-cropping windows
|
|
if ((style & C4ScriptGuiWindowStyleFlag::NoCrop) || !C4GUI::ScrollWindow::IsScrollingNecessary())
|
|
pScrollBar->SetVisibility(false);
|
|
// The "dirty" flag is unset here. Note that it's only used for non "multiple"-style windows after startup.
|
|
// The "multiple"-style windows are updated together when the root window does a full refresh.
|
|
mainWindowNeedsLayoutUpdate = false;
|
|
return true;
|
|
}
|
|
|
|
bool C4ScriptGuiWindow::DrawAll(C4TargetFacet &cgo, int32_t player)
|
|
{
|
|
assert(IsRoot()); // we are root
|
|
if (!IsVisible()) return false;
|
|
// if the viewport shows an upper-board, apply an offset to everything
|
|
const int oldTargetX = cgo.TargetX;
|
|
const int oldTargetY = cgo.TargetY;
|
|
cgo.TargetX += cgo.X;
|
|
cgo.TargetY += cgo.Y;
|
|
// this will check whether the viewport resized and we need an update
|
|
UpdateLayout(cgo);
|
|
// step one: draw all multiple-tagged windows
|
|
DrawChildren(cgo, player, 1);
|
|
// TODO: adjust rectangle for main menu if multiple windows exist
|
|
// step two: draw one "main" menu
|
|
DrawChildren(cgo, player, 0);
|
|
// ..and restore the offset
|
|
cgo.TargetX = oldTargetX;
|
|
cgo.TargetY = oldTargetY;
|
|
return true;
|
|
}
|
|
|
|
bool C4ScriptGuiWindow::Draw(C4TargetFacet &cgo, int32_t player, C4Rect *currentClippingRect)
|
|
{
|
|
assert(!IsRoot()); // not root, root needs to receive DrawAll
|
|
|
|
// message hidden?
|
|
if (!IsVisibleTo(player)) return false;
|
|
|
|
const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
|
|
|
|
if (mainWindowNeedsLayoutUpdate)
|
|
{
|
|
assert(GetParent() && (static_cast<C4ScriptGuiWindow*>(GetParent())->IsRoot()));
|
|
assert(isMainWindow);
|
|
assert(!(style & C4ScriptGuiWindowStyleFlag::Multiple) && "\"Multiple\"-style window not updated by a full refresh of the root window.");
|
|
C4ScriptGuiWindow * parent = static_cast<C4ScriptGuiWindow*>(GetParent());
|
|
UpdateLayout(cgo, parent->rcBounds.Wdt, parent->rcBounds.Hgt);
|
|
assert(!mainWindowNeedsLayoutUpdate);
|
|
}
|
|
|
|
const int32_t outDrawX = cgo.X + cgo.TargetX + rcBounds.x;
|
|
const int32_t outDrawY = cgo.Y + cgo.TargetY + rcBounds.y;
|
|
const int32_t outDrawWdt = rcBounds.Wdt;
|
|
const int32_t outDrawHgt = rcBounds.Hgt;
|
|
const int32_t outDrawRight = outDrawX + rcBounds.Wdt;
|
|
const int32_t outDrawBottom = outDrawY + rcBounds.Hgt;
|
|
// draw various properties
|
|
C4Facet cgoOut(cgo.Surface, outDrawX, outDrawY, outDrawWdt, outDrawHgt);
|
|
|
|
const int32_t &backgroundColor = props[C4ScriptGuiWindowPropertyName::backgroundColor].GetInt();
|
|
if (backgroundColor)
|
|
pDraw->DrawBoxDw(cgo.Surface, outDrawX, outDrawY, outDrawRight - 1.0f, outDrawBottom - 1.0f, backgroundColor);
|
|
|
|
C4GUI::FrameDecoration *frameDecoration = props[C4ScriptGuiWindowPropertyName::frameDecoration].GetFrameDecoration();
|
|
|
|
if (frameDecoration)
|
|
{
|
|
// the frame decoration will adjust for cgo.TargetX/Y itself
|
|
C4Rect rect(
|
|
outDrawX - frameDecoration->iBorderLeft - cgo.TargetX,
|
|
outDrawY - frameDecoration->iBorderTop - cgo.TargetY,
|
|
outDrawWdt + frameDecoration->iBorderRight + frameDecoration->iBorderLeft,
|
|
outDrawHgt + frameDecoration->iBorderBottom + frameDecoration->iBorderTop);
|
|
frameDecoration->Draw(cgo, rect);
|
|
}
|
|
|
|
C4Object *symbolObject = props[C4ScriptGuiWindowPropertyName::symbolObject].GetObject();
|
|
if (symbolObject)
|
|
{
|
|
symbolObject->DrawPicture(cgoOut, false, nullptr);
|
|
}
|
|
else
|
|
{
|
|
C4Def *symbolDef = props[C4ScriptGuiWindowPropertyName::symbolDef].GetDef();
|
|
StdCopyStrBuf *graphicsName = props[C4ScriptGuiWindowPropertyName::symbolGraphicsName].GetStrBuf();
|
|
if (symbolDef)
|
|
{
|
|
symbolDef->Draw(cgoOut, false, 0UL, nullptr, 0, 0, nullptr, graphicsName ? graphicsName->getData() : nullptr);
|
|
}
|
|
}
|
|
|
|
StdCopyStrBuf *strBuf = props[C4ScriptGuiWindowPropertyName::text].GetStrBuf();
|
|
|
|
if (strBuf)
|
|
{
|
|
StdStrBuf sText;
|
|
int alignment = ALeft;
|
|
// If we are showing a scrollbar, we need to leave a bit of space for it so that it doesn't overlap the text.
|
|
const int scrollbarXOffset = pScrollBar->IsVisible() ? pScrollBar->rcBounds.Wdt : 0;
|
|
const int scrollbarScroll = pScrollBar->IsVisible() ? this->GetScrollY() : 0;
|
|
const int actualDrawingWidth = outDrawWdt - scrollbarXOffset;
|
|
|
|
// If we are set to NoCrop, the message will be split by string-defined line breaks only.
|
|
int allowedTextWidth = actualDrawingWidth;
|
|
|
|
if (style & C4ScriptGuiWindowStyleFlag::NoCrop)
|
|
allowedTextWidth = std::numeric_limits<int>::max();
|
|
int32_t textHgt = ::GraphicsResource.FontRegular.BreakMessage(strBuf->getData(), allowedTextWidth, &sText, true);
|
|
float textYOffset = static_cast<float>(-scrollbarScroll), textXOffset = 0.0f;
|
|
if (style & C4ScriptGuiWindowStyleFlag::TextVCenter)
|
|
textYOffset = float(outDrawHgt) / 2.0f - float(textHgt) / 2.0f;
|
|
else if (style & C4ScriptGuiWindowStyleFlag::TextBottom)
|
|
textYOffset = float(outDrawHgt) - float(textHgt);
|
|
if (style & C4ScriptGuiWindowStyleFlag::TextHCenter)
|
|
{
|
|
int wdt, hgt;
|
|
::GraphicsResource.FontRegular.GetTextExtent(sText.getData(), wdt, hgt, true);
|
|
textXOffset = float(actualDrawingWidth) / 2.0f;
|
|
alignment = ACenter;
|
|
}
|
|
else if (style & C4ScriptGuiWindowStyleFlag::TextRight)
|
|
{
|
|
alignment = ARight;
|
|
int wdt, hgt;
|
|
::GraphicsResource.FontRegular.GetTextExtent(sText.getData(), wdt, hgt, true);
|
|
textXOffset = float(actualDrawingWidth);
|
|
}
|
|
pDraw->TextOut(sText.getData(), ::GraphicsResource.FontRegular, 1.0, cgo.Surface, outDrawX + textXOffset, outDrawY + textYOffset, 0xffffffff, alignment);
|
|
}
|
|
|
|
|
|
if (GraphicsSystem.ShowMenuInfo) // print helpful debug info
|
|
{
|
|
DWORD frameColor = C4RGB(100, 150, 100);
|
|
if (currentMouseState & MouseState::Focus) frameColor = C4RGB(0, 255, 0);
|
|
|
|
pDraw->DrawFrameDw(cgo.Surface, outDrawX, outDrawY, outDrawRight, outDrawBottom, frameColor);
|
|
if (target || id)
|
|
{
|
|
StdStrBuf buf = FormatString("%s(%d)", target ? target->GetName() : "", id);
|
|
pDraw->TextOut(buf.getData(), ::GraphicsResource.FontCaption, 1.0, cgo.Surface, cgo.X + outDrawRight, cgo.Y + outDrawBottom - ::GraphicsResource.FontCaption.GetLineHeight(), 0xffff00ff, ARight);
|
|
}
|
|
//StdStrBuf buf2 = FormatString("(%d, %d, %d, %d)", rcBounds.x, rcBounds.y, rcBounds.Wdt, rcBounds.Hgt);
|
|
//pDraw->TextOut(buf2.getData(), ::GraphicsResource.FontCaption, 1.0, cgo.Surface, cgo.X + outDrawX + rcBounds.Wdt / 2, cgo.Y + outDrawY + +rcBounds.Hgt / 2, 0xff00ffff, ACenter);
|
|
}
|
|
|
|
DrawChildren(cgo, player, -1, currentClippingRect);
|
|
return true;
|
|
}
|
|
|
|
bool C4ScriptGuiWindow::GetClippingRect(int32_t &left, int32_t &top, int32_t &right, int32_t &bottom)
|
|
{
|
|
const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
|
|
if (IsRoot() || (style & C4ScriptGuiWindowStyleFlag::NoCrop))
|
|
return false;
|
|
|
|
// Other windows always clip their children.
|
|
// This implicitly clips childrens' text to the parent size, too.
|
|
left = rcBounds.x;
|
|
top = rcBounds.y;
|
|
right = rcBounds.Wdt + left;
|
|
bottom = rcBounds.Hgt + top;
|
|
return true;
|
|
}
|
|
|
|
void C4ScriptGuiWindow::SetTag(C4String *tag)
|
|
{
|
|
// set tag on all properties
|
|
for (uint32_t i = 0; i < C4ScriptGuiWindowPropertyName::_lastProp; ++i)
|
|
if (props[i].SwitchTag(tag))
|
|
{
|
|
// only if tag could have changed position etc.
|
|
if (i <= C4ScriptGuiWindowPropertyName::relBottom || i == C4ScriptGuiWindowPropertyName::text || i == C4ScriptGuiWindowPropertyName::style || i == C4ScriptGuiWindowPropertyName::priority)
|
|
RequestLayoutUpdate();
|
|
}
|
|
|
|
// .. and children
|
|
for (C4GUI::Element * element : *this)
|
|
(static_cast<C4ScriptGuiWindow*>(element))->SetTag(tag);
|
|
}
|
|
|
|
void C4ScriptGuiWindow::MouseEnter(C4GUI::CMouse &)
|
|
{
|
|
assert(::MouseControl.GetPlayer() != NO_OWNER);
|
|
}
|
|
|
|
void C4ScriptGuiWindow::OnMouseIn(int32_t player, int32_t parentOffsetX, int32_t parentOffsetY)
|
|
{
|
|
assert(!HasMouseFocus() && "custom menu window properly loaded incorrectly!");
|
|
currentMouseState = MouseState::Focus;
|
|
|
|
// no need to notify children, this is done in MouseInput
|
|
|
|
// update tooltip info if applicable
|
|
StdCopyStrBuf *strBuf = props[C4ScriptGuiWindowPropertyName::tooltip].GetStrBuf();
|
|
if (strBuf)
|
|
{
|
|
C4Viewport * viewport = ::Viewports.GetViewport(player);
|
|
if (viewport)
|
|
{
|
|
const float guiZoom = viewport->GetGUIZoom();
|
|
const float x = float(parentOffsetX + rcBounds.x) / guiZoom;
|
|
const float y = float(parentOffsetY + rcBounds.y) / guiZoom;
|
|
const float wdt = float(rcBounds.Wdt) / guiZoom;
|
|
const float hgt = float(rcBounds.Hgt) / guiZoom;
|
|
::MouseControl.SetTooltipRectangle(C4Rect(x, y, wdt, hgt));
|
|
::MouseControl.SetTooltipText(*strBuf);
|
|
}
|
|
}
|
|
// execute action
|
|
int32_t actionType = C4ScriptGuiWindowPropertyName::onMouseInAction;
|
|
C4ScriptGuiWindowAction *action = props[actionType].GetAction();
|
|
if (!action) return;
|
|
action->Execute(this, player, actionType);
|
|
}
|
|
|
|
void C4ScriptGuiWindow::MouseLeave(C4GUI::CMouse &)
|
|
{
|
|
assert(::MouseControl.GetPlayer() != NO_OWNER);
|
|
|
|
}
|
|
void C4ScriptGuiWindow::OnMouseOut(int32_t player)
|
|
{
|
|
assert(HasMouseFocus() && "custom menu window probably loaded incorrectly!");
|
|
currentMouseState = MouseState::None;
|
|
|
|
// needs to notify children
|
|
for (C4GUI::Element *iter : *this)
|
|
{
|
|
C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(iter);
|
|
if (child->HasMouseFocus())
|
|
child->OnMouseOut(player);
|
|
}
|
|
|
|
// execute action
|
|
int32_t actionType = C4ScriptGuiWindowPropertyName::onMouseOutAction;
|
|
C4ScriptGuiWindowAction *action = props[actionType].GetAction();
|
|
if (!action) return;
|
|
action->Execute(this, player, actionType);
|
|
}
|
|
|
|
bool C4ScriptGuiWindow::MouseInput(int32_t button, int32_t mouseX, int32_t mouseY, DWORD dwKeyParam)
|
|
{
|
|
// only called on root
|
|
assert(IsRoot());
|
|
// This is only called during a mouse move event, where the MouseControl's player is available.
|
|
const int32_t &player = ::MouseControl.GetPlayer();
|
|
assert(player != NO_OWNER);
|
|
// Only allow one window to catch the mouse input.
|
|
// Do not simply return, however, since other windows might need OnMouseOut().
|
|
bool oneActionAlreadyExecuted = false;
|
|
// non-multiple-windows have a higher priority
|
|
// this is important since they are also drawn on top
|
|
for (int withMultipleFlag = 0; withMultipleFlag <= 1; ++withMultipleFlag)
|
|
{
|
|
for (auto iter = rbegin(); iter != rend(); ++iter)
|
|
{
|
|
C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(*iter);
|
|
|
|
const int32_t &style = child->props[C4ScriptGuiWindowPropertyName::style].GetInt();
|
|
if ((withMultipleFlag == 0) && (style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
|
|
if ((withMultipleFlag == 1) && !(style & C4ScriptGuiWindowStyleFlag::Multiple)) continue;
|
|
|
|
// Do the visibility check first. The child itself won't do it, because we are handling mouse in/out here, too.
|
|
if (!child->IsVisibleTo(player)) continue;
|
|
|
|
// we are root, we have to adjust the position for the "main" windows that are centered
|
|
int32_t adjustedMouseX = 0, adjustedMouseY = mouseY;
|
|
int32_t offsetX = 0, offsetY = 0;
|
|
if (withMultipleFlag == 0)
|
|
{
|
|
offsetX = -rcBounds.x;
|
|
offsetY = -rcBounds.y;
|
|
}
|
|
|
|
adjustedMouseX = mouseX + offsetX;
|
|
adjustedMouseY = mouseY + offsetY;
|
|
|
|
int32_t childLeft = child->rcBounds.x;
|
|
int32_t childRight = child->rcBounds.x + child->rcBounds.Wdt;
|
|
int32_t childTop = child->rcBounds.y;
|
|
int32_t childBottom = child->rcBounds.y + child->rcBounds.Hgt;
|
|
|
|
bool inArea = true;
|
|
if ((adjustedMouseX < childLeft) || (adjustedMouseX > childRight)) inArea = false;
|
|
else if ((adjustedMouseY < childTop) || (adjustedMouseY > childBottom)) inArea = false;
|
|
|
|
if (!inArea) // notify child if it had mouse focus before
|
|
{
|
|
if (child->HasMouseFocus())
|
|
child->OnMouseOut(player);
|
|
continue;
|
|
}
|
|
// Don't break since some more OnMouseOut might be necessary
|
|
if (oneActionAlreadyExecuted) continue;
|
|
|
|
|
|
// keep the mouse coordinates relative to the child's bounds
|
|
if (child->ProcessMouseInput(button, adjustedMouseX - childLeft, adjustedMouseY - childTop - iScrollY, dwKeyParam, childLeft - offsetX, childTop + iScrollY - offsetY))
|
|
{
|
|
oneActionAlreadyExecuted = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return oneActionAlreadyExecuted;
|
|
}
|
|
|
|
bool C4ScriptGuiWindow::ProcessMouseInput(int32_t button, int32_t mouseX, int32_t mouseY, DWORD dwKeyParam, int32_t parentOffsetX, int32_t parentOffsetY)
|
|
{
|
|
const int32_t &player = ::MouseControl.GetPlayer();
|
|
assert(player != NO_OWNER);
|
|
|
|
// completely ignore mouse if the appropriate flag is set
|
|
const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
|
|
if (style & C4ScriptGuiWindowStyleFlag::IgnoreMouse)
|
|
return false;
|
|
|
|
// we have mouse focus! Is this new?
|
|
if (!HasMouseFocus())
|
|
OnMouseIn(player, parentOffsetX, parentOffsetY);
|
|
|
|
// Make sure the UI does not catch release events without matching key-down events.
|
|
// Otherwise, you could e.g. open a menu on left-down and then the menu would block the left-up event, leading to issues.
|
|
if (button == C4MC_Button_LeftUp)
|
|
{
|
|
// Do not catch up-events without prior down-events!
|
|
if (!(currentMouseState & MouseState::MouseDown)) return false;
|
|
}
|
|
|
|
// do not simply break the loop since some OnMouseOut might go missing
|
|
bool oneActionAlreadyExecuted = false;
|
|
|
|
const int32_t scrollAdjustedMouseY = mouseY + iScrollY;
|
|
|
|
// children actually have a higher priority
|
|
bool overChild = false; // remember for later, catch all actions that are in theory over children, even if not reaction (if main window)
|
|
// use reverse iterator since children with higher Priority appear later in the list
|
|
for (auto iter = rbegin(); iter != rend(); ++iter)
|
|
{
|
|
C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(*iter);
|
|
|
|
// Do the visibility check first. The child itself won't do it, because we are handling mouse in/out here, too.
|
|
if (!child->IsVisibleTo(player)) continue;
|
|
|
|
const int32_t childLeft = child->rcBounds.x;
|
|
const int32_t childRight = child->rcBounds.x + child->rcBounds.Wdt;
|
|
const int32_t childTop = child->rcBounds.y;
|
|
const int32_t childBottom = child->rcBounds.y + child->rcBounds.Hgt;
|
|
|
|
bool inArea = true;
|
|
if ((mouseX <= childLeft) || (mouseX > childRight)) inArea = false;
|
|
else if ((scrollAdjustedMouseY <= childTop) || (scrollAdjustedMouseY > childBottom)) inArea = false;
|
|
|
|
if (!inArea) // notify child if it had mouse focus before
|
|
{
|
|
if (child->HasMouseFocus())
|
|
child->OnMouseOut(player);
|
|
continue;
|
|
}
|
|
|
|
if (oneActionAlreadyExecuted) continue;
|
|
|
|
overChild = true;
|
|
// keep coordinates relative to children
|
|
if (child->ProcessMouseInput(button, mouseX - childLeft, scrollAdjustedMouseY - childTop, dwKeyParam, parentOffsetX + rcBounds.x, parentOffsetY + rcBounds.y - iScrollY))
|
|
{
|
|
oneActionAlreadyExecuted = true;
|
|
}
|
|
}
|
|
|
|
if (oneActionAlreadyExecuted) return true;
|
|
|
|
//C4GUI::Element::MouseInput(rMouse, button, mouseX, mouseY, dwKeyParam);
|
|
|
|
// remember button-down events. The action will only be executed on button-up
|
|
// The sequence for double-clicks is LeftDown-LeftUp-LeftDouble-LeftUp, so treat double as down
|
|
if (button == C4MC_Button_LeftDown || button == C4MC_Button_LeftDouble)
|
|
currentMouseState |= MouseState::MouseDown;
|
|
// trigger!
|
|
if (button == C4MC_Button_LeftUp)
|
|
{
|
|
currentMouseState = currentMouseState & ~MouseState::MouseDown;
|
|
C4ScriptGuiWindowAction *action = props[C4ScriptGuiWindowPropertyName::onClickAction].GetAction();
|
|
if (action)
|
|
{
|
|
action->Execute(this, player, C4ScriptGuiWindowPropertyName::onClickAction);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// for scroll-enabled windows, scroll contents with wheel
|
|
if (pScrollBar->IsVisible() && (button == C4MC_Button_Wheel))
|
|
{
|
|
short delta = (short)(dwKeyParam >> 16);
|
|
ScrollBy(-delta);
|
|
//float fac = (lastDrawPosition.bottomMostChild - lastDrawPosition.topMostChild);
|
|
//if (fac == 0.0f) fac = 1.0f;
|
|
//scrollBar->ScrollBy(-float(delta) / fac);
|
|
return true;
|
|
}
|
|
|
|
// forward to scroll-bar if in area
|
|
if (pScrollBar->IsVisible())
|
|
{
|
|
if ((mouseX > pScrollBar->rcBounds.x && mouseX < pScrollBar->rcBounds.x + pScrollBar->rcBounds.Wdt)
|
|
&& (mouseY > pScrollBar->rcBounds.y && mouseY < pScrollBar->rcBounds.y + pScrollBar->rcBounds.Hgt))
|
|
{
|
|
C4GUI::CMouse mouse(mouseX, mouseY);
|
|
if (::MouseControl.IsLeftDown()) mouse.LDown = true;
|
|
pScrollBar->MouseInput(mouse, button, mouseX - pScrollBar->rcBounds.x, mouseY - pScrollBar->rcBounds.y, dwKeyParam);
|
|
}
|
|
}
|
|
|
|
|
|
// if the user still clicked on a menu - even if it didn't do anything, catch it
|
|
// but do that only on the top-level to not stop traversing other branches
|
|
if (isMainWindow)
|
|
return overChild;
|
|
return false;
|
|
}
|
|
|
|
bool C4ScriptGuiWindow::ExecuteCommand(int32_t actionID, int32_t player, int32_t subwindowID, int32_t actionType, C4Object *target)
|
|
{
|
|
if (isMainWindow && subwindowID) // we are a main window! try a shortcut through the ID?
|
|
{
|
|
MenuDebugLogF("passing command... instance:%d, plr:%d, subwin:%d, type:%d [I am %d, MW]", actionID, player, subwindowID, actionType, id);
|
|
MenuDebugLogF("stats:");
|
|
MenuDebugLogF("active menus:\t%d", GetElementCount());
|
|
MenuDebugLogF("children ID map:\t%d", childrenIDMap.size());
|
|
// the reasoning for that shortcut is that I assume that usually windows with actions will also have an ID assigned
|
|
// this obviously doesn't have to be the case, but I believe it's worth the try
|
|
std::pair<std::multimap<int32_t, C4ScriptGuiWindow*>::iterator, std::multimap<int32_t, C4ScriptGuiWindow*>::iterator> range;
|
|
range = childrenIDMap.equal_range(subwindowID);
|
|
|
|
for (std::multimap<int32_t, C4ScriptGuiWindow*>::iterator iter = range.first; iter != range.second; ++iter)
|
|
{
|
|
if (iter->second->ExecuteCommand(actionID, player, subwindowID, actionType, target))
|
|
{
|
|
MenuDebugLogF("shortcutting command sucessful!");
|
|
return true;
|
|
}
|
|
}
|
|
// it is not possible that another window would match the criteria. Abort later after self-check
|
|
MenuDebugLogF("shortcutting command failed.. no appropriate window");
|
|
}
|
|
|
|
// are we elligible?
|
|
if ((id == subwindowID) && (this->target == target))
|
|
{
|
|
MenuDebugLogF("stats: (I am %d)", id);
|
|
MenuDebugLogF("children:\t%d", GetElementCount());
|
|
MenuDebugLogF("all actions:\t%d", props[actionType].GetAllActions().size());
|
|
std::list<C4ScriptGuiWindowAction*> allActions = props[actionType].GetAllActions();
|
|
for (auto action : allActions)
|
|
{
|
|
assert(action && "C4ScriptGuiWindowProperty::GetAllActions returned list with null-pointer");
|
|
|
|
if (action->ExecuteCommand(actionID, this, player))
|
|
{
|
|
MenuDebugLogF("executing command sucessful!");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// note that we should not simply return false here
|
|
// there is no guarantee that only one window with that target&ID exists
|
|
}
|
|
|
|
// not caught, forward to children!
|
|
// abort if main window, though. See above
|
|
if (isMainWindow && subwindowID)
|
|
{
|
|
MenuDebugLogF("executing command failed!");
|
|
return false;
|
|
}
|
|
|
|
// otherwise, just pass to children..
|
|
for (C4GUI::Element *element : *this)
|
|
{
|
|
C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
|
|
if (child->ExecuteCommand(actionID, player, subwindowID, actionType, target))
|
|
{
|
|
MenuDebugLogF("passing command sucessful! (I am %d - &p)", id, this->target);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool C4ScriptGuiWindow::IsRoot()
|
|
{
|
|
return this == Game.ScriptGuiRoot.get();
|
|
}
|
|
|
|
bool C4ScriptGuiWindow::IsVisibleTo(int32_t player)
|
|
{
|
|
// Not visible at all?
|
|
if (!IsVisible()) return false;
|
|
// We have a player assigned and it's a different one?
|
|
const int32_t &myPlayer = props[C4ScriptGuiWindowPropertyName::player].GetInt();
|
|
if (myPlayer != ANY_OWNER && player != myPlayer) return false;
|
|
// We have a target object which is invisible to the player?
|
|
if (target && !target->IsVisible(player, false)) return false;
|
|
// Default to visible!
|
|
return true;
|
|
}
|