forked from Mirrors/openclonk
2073 lines
68 KiB
C++
2073 lines
68 KiB
C++
/*
|
|
* OpenClonk, http://www.openclonk.org
|
|
*
|
|
* Copyright (c) 2013 David Dormagen
|
|
*
|
|
* Portions might be copyrighted by other authors who have contributed
|
|
* to OpenClonk.
|
|
*
|
|
* Permission to use, copy, modify, and/or distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
* See isc_license.txt for full license and disclaimer.
|
|
*
|
|
* "Clonk" is a registered trademark of Matthes Bender.
|
|
* See clonk_trademark_license.txt for full license.
|
|
*/
|
|
|
|
/*
|
|
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 <C4ScriptGuiWindow.h>
|
|
|
|
#include <C4Application.h>
|
|
#include <C4DefList.h>
|
|
#include <C4GraphicsSystem.h>
|
|
#include <C4GraphicsResource.h>
|
|
#include <C4Game.h>
|
|
#include <C4Control.h>
|
|
#include <C4MouseControl.h>
|
|
#include <C4Object.h>
|
|
#include <C4Player.h>
|
|
#include <C4PlayerList.h>
|
|
#include <C4Viewport.h>
|
|
|
|
#include <cmath>
|
|
|
|
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() : 0;
|
|
|
|
if (targetObj == pObj)
|
|
{
|
|
// not only forget object, but completely invalidate action
|
|
action = 0;
|
|
target = 0;
|
|
}
|
|
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();
|
|
|
|
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");
|
|
//LogF("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;
|
|
// 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)
|
|
{
|
|
// 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());
|
|
}
|
|
//LogF("command synced.. target: %x, targetObj: %x, func: %s", target, target->GetObject(), text->GetCStr());
|
|
C4AulParSet Pars(value, C4VInt(player), C4VInt(main->GetID()), C4VInt(parent->GetID()), C4VObj(parent->target));
|
|
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 = 0;
|
|
}
|
|
|
|
void C4ScriptGuiWindowProperty::CleanUp(Prop &prop)
|
|
{
|
|
switch (type)
|
|
{
|
|
case frameDecoration:
|
|
if (prop.deco) delete prop.deco;
|
|
break;
|
|
case onClickAction:
|
|
case onMouseInAction:
|
|
case onMouseOutAction:
|
|
case onCloseAction:
|
|
if (prop.action) delete prop.action;
|
|
break;
|
|
case text:
|
|
if (prop.strBuf) delete prop.strBuf;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void C4ScriptGuiWindowProperty::CleanUpAll()
|
|
{
|
|
for (std::map<C4String*, Prop>::iterator iter = taggedProperties.begin(); iter != taggedProperties.end(); ++iter)
|
|
{
|
|
CleanUp(iter->second);
|
|
if (iter->first != &Strings.P[P_Std])
|
|
iter->first->DecRef();
|
|
}
|
|
}
|
|
|
|
const C4Value C4ScriptGuiWindowProperty::ToC4Value()
|
|
{
|
|
C4PropList *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(std::map<C4String*, Prop>::iterator iter = taggedProperties.begin(); iter != taggedProperties.end(); ++iter)
|
|
{
|
|
C4String *tagString = iter->first;
|
|
const Prop &prop = iter->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:
|
|
val = C4Value(prop.deco ? prop.deco->idSourceDef : C4ID::None);
|
|
break;
|
|
|
|
case C4ScriptGuiWindowPropertyName::text:
|
|
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
|
|
|
|
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)
|
|
{
|
|
C4ValueArray *properties = proplist->GetProperties();
|
|
|
|
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:
|
|
case C4ScriptGuiWindowPropertyName::player:
|
|
current->d = value.getInt();
|
|
break;
|
|
|
|
case C4ScriptGuiWindowPropertyName::symbolObject:
|
|
{
|
|
C4PropList *symbol = value.getPropList();
|
|
if (symbol)
|
|
current->obj = symbol->GetObject();
|
|
else current->obj = 0;
|
|
break;
|
|
}
|
|
case C4ScriptGuiWindowPropertyName::symbolDef:
|
|
{
|
|
C4PropList *symbol = value.getPropList();
|
|
if (symbol)
|
|
current->def = symbol->GetDef();
|
|
else current->def = 0;
|
|
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 = 0;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case C4ScriptGuiWindowPropertyName::text:
|
|
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 (std::map<C4String*, Prop>::iterator iter = taggedProperties.begin(); iter != taggedProperties.end(); ++iter)
|
|
{
|
|
switch (type)
|
|
{
|
|
case C4ScriptGuiWindowPropertyName::symbolObject:
|
|
if (iter->second.obj == pObj)
|
|
iter->second.obj = 0;
|
|
break;
|
|
|
|
case C4ScriptGuiWindowPropertyName::onClickAction:
|
|
case C4ScriptGuiWindowPropertyName::onMouseInAction:
|
|
case C4ScriptGuiWindowPropertyName::onMouseOutAction:
|
|
case C4ScriptGuiWindowPropertyName::onCloseAction:
|
|
if (iter->second.action)
|
|
iter->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 (std::map<C4String*, Prop>::iterator iter = taggedProperties.begin(); iter != taggedProperties.end(); ++iter)
|
|
{
|
|
Prop &p = iter->second;
|
|
if (p.action)
|
|
allActions.push_back(p.action);
|
|
}
|
|
return allActions;
|
|
}
|
|
|
|
|
|
C4ScriptGuiWindow::C4ScriptGuiWindow() : C4GUI::ScrollWindow(this)
|
|
{
|
|
Init();
|
|
}
|
|
|
|
C4ScriptGuiWindow::C4ScriptGuiWindow(float stdBorderX, float stdBorderY) : C4GUI::ScrollWindow(this)
|
|
{
|
|
Init();
|
|
|
|
// set border values for std tag
|
|
// relative offsets are standard, only need to set exact offset
|
|
props[C4ScriptGuiWindowPropertyName::left].SetFloat(Pix2Em(stdBorderX));
|
|
props[C4ScriptGuiWindowPropertyName::right].SetFloat(Pix2Em(-stdBorderX));
|
|
props[C4ScriptGuiWindowPropertyName::top].SetFloat(Pix2Em(stdBorderY));
|
|
props[C4ScriptGuiWindowPropertyName::bottom].SetFloat(Pix2Em(-stdBorderY));
|
|
}
|
|
|
|
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::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(-1);
|
|
|
|
wasRemoved = false;
|
|
closeActionWasExecuted = false;
|
|
currentMouseState = MouseState::None;
|
|
target = 0;
|
|
pScrollBar->fAutoHide = true;
|
|
}
|
|
|
|
C4ScriptGuiWindow::~C4ScriptGuiWindow()
|
|
{
|
|
ClearChildren(false);
|
|
|
|
// delete certain properties that contain allocated elements or referenced strings
|
|
for (int32_t i = 0; i < C4ScriptGuiWindowPropertyName::_lastProp; ++i)
|
|
props[i].CleanUpAll();
|
|
|
|
if (pScrollBar)
|
|
delete pScrollBar;
|
|
}
|
|
|
|
// 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 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 relative, C4ScriptGuiWindowPropertyName 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) return;
|
|
StdStrBuf buf(property.getStr()->GetData());
|
|
|
|
std::string trimmedString;
|
|
size_t maxLength = buf.getLength();
|
|
trimmedString.reserve(maxLength);
|
|
// add all non-whitespace characters to the new string (strtod could abort the parsing otherwise)
|
|
for (size_t i = 0; i < maxLength; ++i)
|
|
{
|
|
if (!isspace(buf[i]))
|
|
trimmedString.push_back(buf[i]);
|
|
}
|
|
|
|
float relativeValue = 0.0;
|
|
float absoluteValue = 0.0;
|
|
|
|
const char *currentPosition = trimmedString.data();
|
|
char *nextPosition;
|
|
const char *lastPosition = trimmedString.data() + trimmedString.size();
|
|
|
|
while (currentPosition < lastPosition)
|
|
{
|
|
// look for next float
|
|
nextPosition = 0;
|
|
float value = static_cast<float>(strtod(currentPosition, &nextPosition));
|
|
|
|
// fail? exit right here (there must be some space left in the string for a unit, too)
|
|
if (currentPosition == nextPosition || nextPosition == 0 || nextPosition >= lastPosition) break;
|
|
|
|
if (*nextPosition == '%')
|
|
{
|
|
relativeValue += value;
|
|
currentPosition = nextPosition + 1;
|
|
}
|
|
else if (*nextPosition == 'e' && *(nextPosition+1) == 'm')
|
|
{
|
|
absoluteValue += value;
|
|
currentPosition = nextPosition + 2;
|
|
}
|
|
else // error, abort!
|
|
break;
|
|
}
|
|
props[relative].SetFloat(relativeValue / 100.0f, tag);
|
|
props[absolute].SetFloat(absoluteValue, tag);
|
|
}
|
|
|
|
// for saving
|
|
C4Value C4ScriptGuiWindow::PositionToC4Value(C4ScriptGuiWindowPropertyName relativeName, C4ScriptGuiWindowPropertyName 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 (std::map<C4String*, C4ScriptGuiWindowProperty::Prop>::iterator iter = relative.taggedProperties.begin(); iter != relative.taggedProperties.end(); ++iter)
|
|
{
|
|
C4String *tag = iter->first;
|
|
StdStrBuf buf;
|
|
buf.Format("%f%%%+fem", iter->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);
|
|
}
|
|
|
|
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 (size_t i = 0; i < entryCount; ++i)
|
|
{
|
|
int32_t prop = toSave[i];
|
|
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_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;
|
|
}
|
|
|
|
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
|
|
C4ValueArray *properties = proplist->GetProperties();
|
|
C4String *stdTag = &Strings.P[P_Std];
|
|
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);
|
|
|
|
C4Value value;
|
|
|
|
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_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)
|
|
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();
|
|
child->name = childName;
|
|
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 (layoutUpdateRequired)
|
|
RequestLayoutUpdate();
|
|
|
|
if (resetStdTag)
|
|
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);
|
|
//LogF("child-map-size: %d, remov %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));
|
|
//LogF("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 0;
|
|
}
|
|
|
|
void C4ScriptGuiWindow::RemoveChild(C4ScriptGuiWindow *child, bool close, bool all)
|
|
{
|
|
// do a layout update asap
|
|
if (!all && !IsRoot())
|
|
RequestLayoutUpdate();
|
|
|
|
if (child && close)
|
|
{
|
|
child->wasRemoved = true;
|
|
child->Close();
|
|
if (child->GetID() != 0)
|
|
ChildWithIDRemoved(child);
|
|
RemoveElement(static_cast<C4GUI::Element*>(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(0, 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);
|
|
}
|
|
|
|
|
|
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
|
|
const float childWdtF = float(child->rcBounds.Wdt)
|
|
+ Em2Pix(child->props[C4ScriptGuiWindowPropertyName::leftMargin].GetFloat()) + Em2Pix(child->props[C4ScriptGuiWindowPropertyName::rightMargin].GetFloat())
|
|
+ float(width) * (child->props[C4ScriptGuiWindowPropertyName::relLeftMargin].GetFloat() + child->props[C4ScriptGuiWindowPropertyName::relRightMargin].GetFloat());
|
|
const float childHgtF = float(child->rcBounds.Hgt)
|
|
+ Em2Pix(child->props[C4ScriptGuiWindowPropertyName::topMargin].GetFloat()) + Em2Pix(child->props[C4ScriptGuiWindowPropertyName::bottomMargin].GetFloat())
|
|
+ float(height) * (child->props[C4ScriptGuiWindowPropertyName::relTopMargin].GetFloat() + child->props[C4ScriptGuiWindowPropertyName::relBottomMargin].GetFloat());
|
|
// do all the rounding after the calculations
|
|
const int32_t childWdt = (int32_t)(childWdtF + 0.5f);
|
|
const int32_t childHgt = (int32_t)(childHgtF + 0.5f);
|
|
// remember the highest child to make sure rows don't overlap
|
|
if (!maxChildHeight || (childHgt > maxChildHeight))
|
|
{
|
|
maxChildHeight = childHgt;
|
|
lowestChildRelY = currentY + childHgt;
|
|
}
|
|
child->rcBounds.x = currentX;
|
|
child->rcBounds.y = currentY;
|
|
|
|
currentX += childWdt + borderX;
|
|
if (currentX + childWdt >= width)
|
|
{
|
|
currentX = borderX;
|
|
currentY += maxChildHeight + borderY;
|
|
maxChildHeight = 0;
|
|
}
|
|
}
|
|
|
|
// do we need a scroll bar?
|
|
EnableScrollBar(currentY > 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.
|
|
const float childHgtF = float(child->rcBounds.Hgt)
|
|
+ Em2Pix(child->props[C4ScriptGuiWindowPropertyName::topMargin].GetFloat()) + Em2Pix(child->props[C4ScriptGuiWindowPropertyName::bottomMargin].GetFloat())
|
|
+ float(rcBounds.Hgt) * (child->props[C4ScriptGuiWindowPropertyName::relTopMargin].GetFloat() + child->props[C4ScriptGuiWindowPropertyName::relBottomMargin].GetFloat());
|
|
const int32_t childHgt = (int32_t)(childHgtF + 0.5f);
|
|
|
|
child->rcBounds.y = currentY;
|
|
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.TargetX + clipX1;
|
|
const int32_t targetClipY1 = cgo.TargetY + clipY1;
|
|
const int32_t targetClipX2 = cgo.TargetX + clipX2;
|
|
const int32_t targetClipY2 = cgo.TargetY + clipY2;
|
|
|
|
if (clipping)
|
|
{
|
|
myClippingRect = C4Rect(targetClipX1, targetClipY1, targetClipX2, 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 iter = begin(); iter != end(); ++iter)
|
|
{
|
|
C4GUI::Element *element = *iter;
|
|
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;
|
|
if (pScrollBar->IsVisible())
|
|
pScrollBar->DrawElement(cgo);
|
|
|
|
if (IsRoot())
|
|
{
|
|
pDraw->RestorePrimaryClipper();
|
|
}
|
|
|
|
// restore target rectangle
|
|
cgo.TargetX = oldTargetX;
|
|
cgo.TargetY = oldTargetY;
|
|
return oneDrawn;
|
|
}
|
|
|
|
void C4ScriptGuiWindow::RequestLayoutUpdate()
|
|
{
|
|
if (isMainWindow)
|
|
{
|
|
const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
|
|
|
|
if (!(style & C4ScriptGuiWindowStyleFlag::Multiple)) // are we a simple centered window?
|
|
mainWindowNeedsLayoutUpdate = true;
|
|
else // we are one of the multiple windows.. the root better do a full refresh
|
|
static_cast<C4ScriptGuiWindow*>(GetParent())->mainWindowNeedsLayoutUpdate = true;
|
|
}
|
|
else 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 whole viewport for drawing - minus some standard border
|
|
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();
|
|
|
|
float fullWidth = cgo.Wdt * cgo.Zoom;
|
|
float fullHeight = cgo.Hgt * cgo.Zoom;
|
|
|
|
float wdt = fullWidth - Em2Pix(left) + Em2Pix(right);
|
|
float hgt = fullHeight - Em2Pix(top) + Em2Pix(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 = Em2Pix(left);
|
|
rcBounds.y = Em2Pix(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
|
|
StdCopyStrBuf *strBuf = props[C4ScriptGuiWindowPropertyName::text].GetStrBuf();
|
|
if (strBuf)
|
|
{
|
|
StdStrBuf sText;
|
|
int32_t textHgt = ::GraphicsResource.FontRegular.BreakMessage(strBuf->getData(), rcBounds.Wdt, &sText, true);
|
|
// enable auto scroll
|
|
if (textHgt > rcBounds.Hgt)
|
|
rcBounds.Hgt = textHgt;
|
|
}
|
|
|
|
UpdateChildLayout(cgo, width, height);
|
|
|
|
// update scroll bar
|
|
// C4GUI::ScrollWindow::UpdateOwnPos();
|
|
|
|
// special layout selected?
|
|
if (style & C4ScriptGuiWindowStyleFlag::GridLayout)
|
|
UpdateLayoutGrid();
|
|
else if (style & C4ScriptGuiWindowStyleFlag::VerticalLayout)
|
|
UpdateLayoutVertical();
|
|
|
|
// check if we need a scroll-bar
|
|
int32_t topMostChild = 0;
|
|
int32_t bottomMostChild = rcBounds.Hgt;
|
|
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;
|
|
}
|
|
// subtract one against rounding errors
|
|
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();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool C4ScriptGuiWindow::DrawAll(C4TargetFacet &cgo, int32_t player)
|
|
{
|
|
assert(IsRoot()); // we are root
|
|
if (!IsVisible()) return false;
|
|
// 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);
|
|
return true;
|
|
}
|
|
|
|
bool C4ScriptGuiWindow::Draw(C4TargetFacet &cgo, int32_t player, C4Rect *currentClippingRect)
|
|
{
|
|
assert(!IsRoot()); // not root, root needs to receive DrawAll
|
|
|
|
// message hidden?
|
|
const int32_t &myPlayer = props[C4ScriptGuiWindowPropertyName::player].GetInt();
|
|
if (!IsVisible() || (myPlayer != -1 && player != myPlayer) || (target && !target->IsVisible(player, false)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (mainWindowNeedsLayoutUpdate)
|
|
{
|
|
assert(GetParent() && (static_cast<C4ScriptGuiWindow*>(GetParent())->IsRoot()));
|
|
assert(isMainWindow);
|
|
C4ScriptGuiWindow * parent = static_cast<C4ScriptGuiWindow*>(GetParent());
|
|
UpdateLayout(cgo, parent->rcBounds.Wdt, parent->rcBounds.Hgt);
|
|
mainWindowNeedsLayoutUpdate = false;
|
|
}
|
|
|
|
|
|
|
|
float childOffsetY = 0.0f; // for scrolling
|
|
|
|
// check whether we are scrolling
|
|
//float childHgt = lastDrawPosition.bottomMostChild - lastDrawPosition.topMostChild;
|
|
|
|
//if (scrollBar)
|
|
// childOffsetY = -1.0f * (scrollBar->offset * (childHgt - rcBounds.Hgt));
|
|
|
|
const int32_t outDrawX = cgo.TargetX + rcBounds.x;
|
|
const int32_t outDrawY = 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, NULL);
|
|
}
|
|
else
|
|
{
|
|
C4Def *symbolDef = props[C4ScriptGuiWindowPropertyName::symbolDef].GetDef();
|
|
if (symbolDef)
|
|
{
|
|
symbolDef->Draw(cgoOut);
|
|
}
|
|
}
|
|
|
|
StdCopyStrBuf *strBuf = props[C4ScriptGuiWindowPropertyName::text].GetStrBuf();
|
|
|
|
if (strBuf)
|
|
{
|
|
StdStrBuf sText;
|
|
int alignment = ALeft;
|
|
int32_t textHgt = ::GraphicsResource.FontRegular.BreakMessage(strBuf->getData(), outDrawWdt, &sText, true);
|
|
float textYOffset = 0.0f, 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(outDrawWdt)/ 2.0f - float(wdt) / 2.0f;
|
|
}
|
|
else if (style & C4ScriptGuiWindowStyleFlag::TextRight)
|
|
{
|
|
alignment = ARight;
|
|
int wdt, hgt;
|
|
::GraphicsResource.FontRegular.GetTextExtent(sText.getData(), wdt, hgt, true);
|
|
textXOffset = float(outDrawWdt) - float(wdt);
|
|
}
|
|
pDraw->TextOut(sText.getData(), ::GraphicsResource.FontRegular, 1.0, cgo.Surface, outDrawX + textXOffset, outDrawY + textYOffset, 0xffffffff, ALeft);
|
|
}
|
|
|
|
|
|
if (GraphicsSystem.ShowMenuInfo) // print helpful debug info
|
|
{
|
|
C4ScriptGuiWindow * parent = static_cast<C4ScriptGuiWindow*>(GetParent());
|
|
|
|
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() || isMainWindow || (style & C4ScriptGuiWindowStyleFlag::NoCrop))
|
|
return false;
|
|
|
|
if (pScrollBar->IsVisible())
|
|
{
|
|
left = rcBounds.x;
|
|
top = rcBounds.y;
|
|
right = rcBounds.Wdt + left;
|
|
bottom = rcBounds.Hgt + top;
|
|
return true;
|
|
}
|
|
|
|
/*const int32_t &style = props[C4ScriptGuiWindowPropertyName::style].GetInt();
|
|
if (!isMainWindow && !(style & C4ScriptGuiWindowStyleFlag::NoCrop))
|
|
return static_cast<C4ScriptGuiWindow*>(GetParent())->GetClippingRect(left, top, right, bottom);
|
|
*/
|
|
return false;
|
|
}
|
|
|
|
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()
|
|
{
|
|
const int32_t &player = ::MouseControl.GetPlayer();
|
|
assert(player != 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()
|
|
{
|
|
const int32_t &player = ::MouseControl.GetPlayer();
|
|
assert(player != 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());
|
|
// 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;
|
|
|
|
// 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;
|
|
//LogF("%d|%d in %d|%d // %d|%d", mouseX, mouseY, childLeft, childTop, childRight, childBottom);
|
|
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;
|
|
|
|
// if the window belongs to an invisible object, don't show
|
|
// the "normal" visibility will be handed by the parent callback
|
|
if (target)
|
|
if (!target->IsVisible(player, false))
|
|
return false;
|
|
|
|
// we have mouse focus! Is this new?
|
|
if (!HasMouseFocus())
|
|
OnMouseIn(player, parentOffsetX, parentOffsetY);
|
|
|
|
// 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);
|
|
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
|
|
if (button == C4MC_Button_LeftDown)
|
|
currentMouseState |= MouseState::MouseDown;
|
|
// trigger!
|
|
if (button == C4MC_Button_LeftUp && (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?
|
|
{
|
|
//LogF("passing command... %d, %d, %d, %d, %d [I am %d, MW]", actionID, player, subwindowID, actionType, tag, id);
|
|
// 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))
|
|
return true;
|
|
}
|
|
// it is not possible that another window would match the criteria. Abort later after self-check
|
|
}
|
|
|
|
// are we elligible?
|
|
if ((id == subwindowID) && (this->target == target))
|
|
{
|
|
|
|
std::list<C4ScriptGuiWindowAction*> allActions = props[actionType].GetAllActions();
|
|
for (std::list<C4ScriptGuiWindowAction*>::iterator iter = allActions.begin(); iter != allActions.end(); ++iter)
|
|
{
|
|
C4ScriptGuiWindowAction *action = *iter;
|
|
assert(action && "C4ScriptGuiWindowProperty::GetAllActions returned list with null-pointer");
|
|
|
|
if (action->ExecuteCommand(actionID, this, player))
|
|
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) return false;
|
|
|
|
//for (std::list<C4ScriptGuiWindow*>::iterator iter = children.begin(); iter != children.end(); ++iter)
|
|
for (C4GUI::Element *element : *this)
|
|
{
|
|
C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
|
|
if (child->ExecuteCommand(actionID, player, subwindowID, actionType, target))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool C4ScriptGuiWindow::IsRoot()
|
|
{
|
|
return this == Game.ScriptGuiRoot;
|
|
}
|