/*
* This source file is part of RmlUi, the HTML/CSS Interface Middleware
*
* For the latest information, see http://github.com/mikke89/RmlUi
*
* Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
* Copyright (c) 2019-2023 The RmlUi Team, and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#include "DataModel.h"
#include "../../Include/RmlUi/Core/DataTypeRegister.h"
#include "../../Include/RmlUi/Core/Element.h"
#include "DataController.h"
#include "DataView.h"
namespace Rml {
static DataAddress ParseAddress(const String& address_str)
{
StringList list;
StringUtilities::ExpandString(list, address_str, '.');
DataAddress address;
address.reserve(list.size() * 2);
for (const auto& item : list)
{
if (item.empty())
return DataAddress();
size_t i_open = item.find('[', 0);
if (i_open == 0)
return DataAddress();
address.emplace_back(item.substr(0, i_open));
while (i_open != String::npos)
{
size_t i_close = item.find(']', i_open + 1);
if (i_close == String::npos)
return DataAddress();
int index = FromString(item.substr(i_open + 1, i_close - i_open), -1);
if (index < 0)
return DataAddress();
address.emplace_back(index);
i_open = item.find('[', i_close + 1);
}
// TODO: Abort on invalid characters among [ ] and after the last found bracket?
}
RMLUI_ASSERT(!address.empty() && !address[0].name.empty());
return address;
}
// Returns an error string on error, or nullptr on success.
static const char* LegalVariableName(const String& name)
{
static SmallUnorderedSet reserved_names{"it", "it_index", "ev", "true", "false", "size", "literal"};
if (name.empty())
return "Name cannot be empty.";
const String name_lower = StringUtilities::ToLower(name);
const char first = name_lower.front();
if (!(first >= 'a' && first <= 'z'))
return "First character must be 'a-z' or 'A-Z'.";
for (const char c : name_lower)
{
if (!(c == '_' || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')))
return "Name must strictly contain characters a-z, A-Z, 0-9 and under_score.";
}
if (reserved_names.count(name_lower) == 1)
return "Name is reserved.";
return nullptr;
}
static String DataAddressToString(const DataAddress& address)
{
String result;
bool is_first = true;
for (auto& entry : address)
{
if (entry.index >= 0)
result += '[' + ToString(entry.index) + ']';
else
{
if (!is_first)
result += ".";
result += entry.name;
}
is_first = false;
}
return result;
}
DataModel::DataModel(DataTypeRegister* data_type_register) : data_type_register(data_type_register)
{
views = MakeUnique();
controllers = MakeUnique();
}
DataModel::~DataModel()
{
RMLUI_ASSERT(attached_elements.empty());
}
void DataModel::AddView(DataViewPtr view)
{
views->Add(std::move(view));
}
void DataModel::AddController(DataControllerPtr controller)
{
controllers->Add(std::move(controller));
}
bool DataModel::BindVariable(const String& name, DataVariable variable)
{
const char* name_error_str = LegalVariableName(name);
if (name_error_str)
{
Log::Message(Log::LT_WARNING, "Could not bind data variable '%s'. %s", name.c_str(), name_error_str);
return false;
}
if (!variable)
{
Log::Message(Log::LT_WARNING, "Could not bind variable '%s' to data model, data type not registered.", name.c_str());
return false;
}
bool inserted = variables.emplace(name, variable).second;
if (!inserted)
{
Log::Message(Log::LT_WARNING, "Data model variable with name '%s' already exists.", name.c_str());
return false;
}
return true;
}
bool DataModel::BindFunc(const String& name, DataGetFunc get_func, DataSetFunc set_func)
{
auto result = function_variable_definitions.emplace(name, nullptr);
auto& it = result.first;
bool inserted = result.second;
if (!inserted)
{
Log::Message(Log::LT_ERROR, "Data get/set function with name %s already exists in model", name.c_str());
return false;
}
auto& func_definition_ptr = it->second;
func_definition_ptr = MakeUnique(std::move(get_func), std::move(set_func));
return BindVariable(name, DataVariable(func_definition_ptr.get(), nullptr));
}
bool DataModel::BindEventCallback(const String& name, DataEventFunc event_func)
{
const char* name_error_str = LegalVariableName(name);
if (name_error_str)
{
Log::Message(Log::LT_WARNING, "Could not bind data event callback '%s'. %s", name.c_str(), name_error_str);
return false;
}
if (!event_func)
{
Log::Message(Log::LT_WARNING, "Could not bind data event callback '%s' to data model, empty function provided.", name.c_str());
return false;
}
bool inserted = event_callbacks.emplace(name, std::move(event_func)).second;
if (!inserted)
{
Log::Message(Log::LT_WARNING, "Data event callback with name '%s' already exists.", name.c_str());
return false;
}
return true;
}
bool DataModel::InsertAlias(Element* element, const String& alias_name, DataAddress replace_with_address)
{
if (replace_with_address.empty() || replace_with_address.front().name.empty())
{
Log::Message(Log::LT_WARNING, "Could not add alias variable '%s' to data model, replacement address invalid.", alias_name.c_str());
return false;
}
if (variables.count(alias_name) == 1)
Log::Message(Log::LT_WARNING, "Alias variable '%s' is shadowed by a global variable.", alias_name.c_str());
auto& map = aliases.emplace(element, SmallUnorderedMap()).first->second;
auto it = map.find(alias_name);
if (it != map.end())
Log::Message(Log::LT_WARNING, "Alias name '%s' in data model already exists, replaced.", alias_name.c_str());
map[alias_name] = std::move(replace_with_address);
return true;
}
bool DataModel::EraseAliases(Element* element)
{
return aliases.erase(element) == 1;
}
void DataModel::CopyAliases(Element* from_element, Element* to_element)
{
if (from_element == to_element)
return;
auto existing_map = aliases.find(from_element);
if (existing_map != aliases.end())
{
// Need to create a copy to prevent errors during concurrent modification for 3rd party containers
auto copy = existing_map->second;
for (const auto& it : copy)
aliases[to_element][it.first] = std::move(it.second);
}
}
DataAddress DataModel::ResolveAddress(const String& address_str, Element* element) const
{
DataAddress address = ParseAddress(address_str);
if (address.empty())
return address;
const String& first_name = address.front().name;
auto it = variables.find(first_name);
if (it != variables.end())
return address;
// Look for a variable alias for the first name.
Element* ancestor = element;
while (ancestor && ancestor->GetDataModel() == this)
{
auto it_element = aliases.find(ancestor);
if (it_element != aliases.end())
{
const auto& alias_names = it_element->second;
auto it_alias_name = alias_names.find(first_name);
if (it_alias_name != alias_names.end())
{
const DataAddress& replace_address = it_alias_name->second;
if (replace_address.empty() || replace_address.front().name.empty())
{
// Variable alias is invalid
return DataAddress();
}
// Insert the full alias address, replacing the first element.
address[0] = replace_address[0];
address.insert(address.begin() + 1, replace_address.begin() + 1, replace_address.end());
return address;
}
}
ancestor = ancestor->GetParentNode();
}
Log::Message(Log::LT_WARNING, "Could not find variable name '%s' in data model.", address_str.c_str());
return DataAddress();
}
DataVariable DataModel::GetVariable(const DataAddress& address) const
{
if (address.empty())
return DataVariable();
auto it = variables.find(address.front().name);
if (it != variables.end())
{
DataVariable variable = it->second;
for (int i = 1; i < (int)address.size() && variable; i++)
{
variable = variable.Child(address[i]);
if (!variable)
return DataVariable();
}
return variable;
}
if (address[0].name == "literal")
{
if (address.size() > 2 && address[1].name == "int")
return MakeLiteralIntVariable(address[2].index);
}
return DataVariable();
}
const DataEventFunc* DataModel::GetEventCallback(const String& name)
{
auto it = event_callbacks.find(name);
if (it == event_callbacks.end())
{
Log::Message(Log::LT_WARNING, "Could not find data event callback '%s' in data model.", name.c_str());
return nullptr;
}
return &it->second;
}
bool DataModel::GetVariableInto(const DataAddress& address, Variant& out_value) const
{
DataVariable variable = GetVariable(address);
bool result = (variable && variable.Get(out_value));
if (!result)
Log::Message(Log::LT_WARNING, "Could not get value from data variable '%s'.", DataAddressToString(address).c_str());
return result;
}
void DataModel::DirtyVariable(const String& variable_name)
{
// RMLUI_ASSERTMSG(LegalVariableName(variable_name) == nullptr, "Illegal variable name provided. Only top-level variables can be dirtied.");
// RMLUI_ASSERTMSG(variables.count(variable_name) == 1, "In DirtyVariable: Variable name not found among added variables.");
dirty_variables.emplace(variable_name);
}
bool DataModel::IsVariableDirty(const String& variable_name) const
{
RMLUI_ASSERTMSG(LegalVariableName(variable_name) == nullptr, "Illegal variable name provided. Only top-level variables can be dirtied.");
return dirty_variables.count(variable_name) == 1;
}
void DataModel::DirtyAllVariables()
{
dirty_variables.reserve(variables.size());
for (const auto& variable : variables)
{
dirty_variables.emplace(variable.first);
}
}
bool DataModel::CallTransform(const String& name, const VariantList& arguments, Variant& out_result) const
{
if (const auto transform_register = data_type_register->GetTransformFuncRegister())
return transform_register->Call(name, arguments, out_result);
return false;
}
void DataModel::AttachModelRootElement(Element* element)
{
attached_elements.insert(element);
}
ElementList DataModel::GetAttachedModelRootElements() const
{
return ElementList(attached_elements.begin(), attached_elements.end());
}
void DataModel::OnElementRemove(Element* element)
{
EraseAliases(element);
views->OnElementRemove(element);
controllers->OnElementRemove(element);
attached_elements.erase(element);
}
bool DataModel::Update(bool clear_dirty_variables)
{
const bool result = views->Update(*this, dirty_variables);
if (clear_dirty_variables)
dirty_variables.clear();
return result;
}
} // namespace Rml