306 lines
6.7 KiB
C++
306 lines
6.7 KiB
C++
#include "configlist.hpp"
|
|
|
|
#include <QJsonObject>
|
|
#include <QMetaObject>
|
|
#include <QQmlEngine>
|
|
#include <QString>
|
|
|
|
namespace ZShell::config {
|
|
|
|
ConfigList::ConfigList(QObject* parent, const QVariantList& defaults)
|
|
: ConfigNode(parent), m_defaults(QJsonArray::fromVariantList(defaults)) {}
|
|
|
|
int ConfigList::count() const {
|
|
return static_cast<int>(m_items.size());
|
|
}
|
|
|
|
QVariantList ConfigList::values() const {
|
|
QVariantList vals;
|
|
vals.reserve(m_items.size());
|
|
for (auto* const item : m_items)
|
|
vals.append(QVariant::fromValue(item));
|
|
return vals;
|
|
}
|
|
|
|
ConfigObject* ConfigList::itemAt(int index) const {
|
|
if (index < 0 || index >= m_items.size()) return nullptr;
|
|
|
|
return m_items.at(index);
|
|
}
|
|
|
|
const QList<ConfigObject*>& ConfigList::items() const {
|
|
return m_items;
|
|
}
|
|
|
|
void ConfigList::remove(int index) {
|
|
if (index < 0 || index >= m_items.size()) {
|
|
qCWarning(lcConfig)
|
|
<< metaObject()->className() << "cannot remove index" << index
|
|
<< "of" << count();
|
|
return;
|
|
}
|
|
|
|
destroyItem(m_items.takeAt(index));
|
|
|
|
m_loaded = true;
|
|
emit countChanged();
|
|
emit valuesChanged();
|
|
notifyChanged();
|
|
}
|
|
|
|
void ConfigList::move(int from, int to) {
|
|
if (from < 0 || from >= m_items.size() || to < 0 || to >= m_items.size()) {
|
|
qCWarning(lcConfig) << metaObject()->className() << "cannot move"
|
|
<< from << "to" << to << "of" << count();
|
|
return;
|
|
}
|
|
|
|
if (from == to) return;
|
|
|
|
m_items.move(from, to);
|
|
|
|
m_loaded = true;
|
|
emit valuesChanged();
|
|
notifyChanged();
|
|
}
|
|
|
|
void ConfigList::clear() {
|
|
if (m_items.isEmpty()) return;
|
|
|
|
destroyItems();
|
|
|
|
m_loaded = true;
|
|
emit countChanged();
|
|
emit valuesChanged();
|
|
notifyChanged();
|
|
}
|
|
|
|
void ConfigList::loadFromJson(const QJsonValue& json) {
|
|
if (!json.isArray()) {
|
|
qCWarning(
|
|
lcConfig,
|
|
"Option '%s' must be a list, ignoring",
|
|
qUtf8Printable(propertyPath()));
|
|
m_rejectedJson = json;
|
|
return;
|
|
}
|
|
|
|
m_rejectedJson = QJsonValue::Undefined;
|
|
populate(json.toArray());
|
|
m_loaded = true;
|
|
}
|
|
|
|
QJsonValue ConfigList::toJson() const {
|
|
if (m_loaded) return elementsToJson();
|
|
|
|
return m_rejectedJson;
|
|
}
|
|
|
|
void ConfigList::clearLoadedKeys() {
|
|
m_loaded = false;
|
|
m_rejectedJson = QJsonValue::Undefined;
|
|
}
|
|
|
|
QStringList ConfigList::unknownKeys() const {
|
|
QStringList keys;
|
|
|
|
for (int i = 0; i < m_items.size(); ++i) {
|
|
const auto childKeys = m_items.at(i)->unknownKeys();
|
|
for (const auto& childKey : childKeys)
|
|
keys.append(joinPath(QString("[%1]").arg(i), childKey));
|
|
}
|
|
|
|
return keys;
|
|
}
|
|
|
|
void ConfigList::resyncFromGlobal() {
|
|
syncValuesFromGlobal();
|
|
}
|
|
|
|
ConfigObject* ConfigList::insertItem(const QVariantMap& props, int index) {
|
|
const auto pos = index < 0 || index > m_items.size() ? m_items.size()
|
|
: index;
|
|
|
|
appendItem(QJsonObject::fromVariantMap(props));
|
|
m_items.move(m_items.size() - 1, pos);
|
|
|
|
auto* const item = m_items.at(pos);
|
|
const auto unknown = item->unknownKeys();
|
|
for (const auto& key : unknown)
|
|
qCWarning(lcConfig) << "Unknown option" << key << "for"
|
|
<< item->metaObject()->className();
|
|
|
|
m_loaded = true;
|
|
emit countChanged();
|
|
emit valuesChanged();
|
|
notifyChanged();
|
|
|
|
return item;
|
|
}
|
|
|
|
QJsonArray ConfigList::elementsToJson() const {
|
|
QJsonArray arr;
|
|
|
|
for (auto* const item : m_items)
|
|
arr.append(item->toJson().toObject());
|
|
|
|
return arr;
|
|
}
|
|
|
|
void ConfigList::resetToDefaults() {
|
|
loadFromJsonQuietly(m_defaults);
|
|
m_loaded = false;
|
|
}
|
|
|
|
void ConfigList::syncValuesFromGlobal() {
|
|
if (m_loaded) return;
|
|
|
|
if (auto* const global = qobject_cast<ConfigList*>(m_global))
|
|
populate(global->elementsToJson());
|
|
}
|
|
|
|
void ConfigList::onGlobalPropertiesChanged(const QMap<QString, QVariant>&) {
|
|
syncValuesFromGlobal();
|
|
}
|
|
|
|
QString ConfigList::childPath(const ConfigNode* child) const {
|
|
for (int i = 0; i < m_items.size(); ++i)
|
|
if (m_items.at(i) == child) return QString("[%1]").arg(i);
|
|
|
|
return {};
|
|
}
|
|
|
|
void ConfigList::populate(const QJsonArray& arr) {
|
|
const auto current = elementsToJson();
|
|
if (current == arr) return;
|
|
|
|
const auto old = m_items;
|
|
QList<bool> reused(old.size(), false);
|
|
|
|
m_items.clear();
|
|
m_items.reserve(arr.size());
|
|
|
|
for (int i = 0; i < arr.size(); ++i) {
|
|
auto match =
|
|
i < old.size() && !reused.at(i) && current.at(i) == arr.at(i) ? i
|
|
: -1;
|
|
|
|
for (int j = 0; match < 0 && j < old.size(); ++j)
|
|
if (!reused.at(j) && current.at(j) == arr.at(i)) match = j;
|
|
|
|
auto update = false;
|
|
for (int j = 0; match < 0 && j < old.size(); ++j) {
|
|
if (!reused.at(j) &&
|
|
sameElement(old.at(j), current.at(j), arr.at(i))) {
|
|
match = j;
|
|
update = true;
|
|
}
|
|
}
|
|
|
|
if (match < 0) {
|
|
appendItem(arr.at(i));
|
|
continue;
|
|
}
|
|
|
|
reused[match] = true;
|
|
|
|
if (update) old.at(match)->loadFromJsonQuietly(arr.at(i));
|
|
|
|
m_items.append(old.at(match));
|
|
}
|
|
|
|
for (int i = 0; i < old.size(); ++i)
|
|
if (!reused.at(i)) destroyItem(old.at(i));
|
|
|
|
if (m_items.size() != old.size()) emit countChanged();
|
|
|
|
emit valuesChanged();
|
|
|
|
notifyChanged();
|
|
}
|
|
|
|
bool ConfigList::sameElement(
|
|
const ConfigObject* item,
|
|
const QJsonValue& current,
|
|
const QJsonValue& json) {
|
|
const auto keys = item->identityKeys();
|
|
if (keys.isEmpty()) return false;
|
|
|
|
const auto currentObj = current.toObject();
|
|
const auto newObj = json.toObject();
|
|
|
|
if (currentObj.keys() != newObj.keys()) return false;
|
|
|
|
for (const auto& key : keys)
|
|
if (!newObj.contains(key) || currentObj.value(key) != newObj.value(key))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
void ConfigList::appendItem(const QJsonValue& json) {
|
|
auto* const item = createItem();
|
|
QQmlEngine::setObjectOwnership(item, QQmlEngine::CppOwnership);
|
|
|
|
if (m_items.isEmpty()) rejectGlobalOnlyElement(item);
|
|
|
|
item->loadFromJsonQuietly(json);
|
|
|
|
connectItem(item);
|
|
|
|
m_items.append(item);
|
|
}
|
|
|
|
void ConfigList::rejectGlobalOnlyElement(const ConfigObject* item) const {
|
|
const auto keys = item->globalOnlyKeys();
|
|
if (keys.isEmpty()) return;
|
|
|
|
qCCritical(
|
|
lcConfig,
|
|
"%s declares global-only options (%s) which do not work inside a list, "
|
|
"use CONFIG_GLOBAL_LIST on %s instead",
|
|
item->metaObject()->className(),
|
|
qUtf8Printable(keys.join(", ")),
|
|
metaObject()->className());
|
|
}
|
|
|
|
void ConfigList::connectItem(ConfigNode* node) {
|
|
connect(
|
|
node, &ConfigNode::propertiesChanged, this, &ConfigList::onItemChanged);
|
|
|
|
const auto children = node->childNodes();
|
|
for (auto* const child : children)
|
|
connectItem(child);
|
|
}
|
|
|
|
void ConfigList::disconnectItem(ConfigNode* node) {
|
|
node->disconnect(this);
|
|
|
|
const auto children = node->childNodes();
|
|
for (auto* const child : children)
|
|
disconnectItem(child);
|
|
}
|
|
|
|
void ConfigList::destroyItems() {
|
|
for (auto* const item : std::as_const(m_items))
|
|
destroyItem(item);
|
|
|
|
m_items.clear();
|
|
}
|
|
|
|
void ConfigList::destroyItem(ConfigObject* item) {
|
|
disconnectItem(item);
|
|
item->deleteLater();
|
|
}
|
|
|
|
void ConfigList::onItemChanged() {
|
|
m_loaded = true;
|
|
notifyChanged();
|
|
}
|
|
|
|
void ConfigList::notifyChanged() {
|
|
notifyPropertyChanged("values", count());
|
|
}
|
|
|
|
} // namespace ZShell::config
|