2023-06-12 10:46:54 +02:00
|
|
|
/*
|
|
|
|
|
* This file is part of the Flowee project
|
2025-02-01 17:17:57 +01:00
|
|
|
* Copyright (C) 2023-2025 Tom Zander <tom@flowee.org>
|
2023-06-12 10:46:54 +02:00
|
|
|
*
|
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
*/
|
2023-06-11 19:06:16 +02:00
|
|
|
#include "ModuleManager.h"
|
|
|
|
|
|
|
|
|
|
#include <QFile>
|
2023-06-12 10:46:54 +02:00
|
|
|
#include <QCoreApplication>
|
|
|
|
|
#include <QLocale>
|
2023-06-12 15:26:31 +02:00
|
|
|
#include <QStandardPaths>
|
2023-06-12 10:46:54 +02:00
|
|
|
#include <QTranslator>
|
2023-06-13 13:02:38 +02:00
|
|
|
#include <QGuiApplication>
|
2025-10-20 21:50:50 +02:00
|
|
|
#include <QTimer>
|
2023-06-11 19:06:16 +02:00
|
|
|
|
2023-06-13 13:02:38 +02:00
|
|
|
#include <ripemd160.h>
|
|
|
|
|
#include <Logger.h>
|
|
|
|
|
#include <streaming/BufferPool.h>
|
2024-10-08 21:08:06 +02:00
|
|
|
#include <streaming/BufferPools.h>
|
2023-06-13 13:02:38 +02:00
|
|
|
#include <streaming/MessageBuilder.h>
|
|
|
|
|
#include <streaming/MessageParser.h>
|
|
|
|
|
|
|
|
|
|
#include <fstream>
|
2023-06-12 15:26:31 +02:00
|
|
|
#include <boost/filesystem.hpp>
|
2023-06-11 19:06:16 +02:00
|
|
|
|
2023-06-13 13:02:38 +02:00
|
|
|
enum ModuleConfigSaveTags {
|
|
|
|
|
ModuleId = 1,
|
2024-10-24 22:09:40 +02:00
|
|
|
LegacyModuleSectionId, // Deprecated (since 2024-10)
|
|
|
|
|
LegacyModuleSectionEnabled, // Deprecated (since 2024-10)
|
|
|
|
|
ModuleEnabled, // bool
|
|
|
|
|
ModuleSaveData // byte-array for a module to save local config.
|
2023-06-13 13:02:38 +02:00
|
|
|
};
|
|
|
|
|
|
2025-11-12 00:18:14 +01:00
|
|
|
static ModuleManager::FrontEnd g_moduleManager_FrontEnd = ModuleManager::Desktop;
|
|
|
|
|
|
2023-06-13 13:02:38 +02:00
|
|
|
|
2023-06-11 19:06:16 +02:00
|
|
|
ModuleManager::ModuleManager(QObject *parent)
|
|
|
|
|
: QObject(parent)
|
|
|
|
|
{
|
2023-06-12 15:26:31 +02:00
|
|
|
m_configFile = QStandardPaths::locate(QStandardPaths::AppConfigLocation, "modules.conf");
|
|
|
|
|
if (m_configFile.isEmpty()) {
|
|
|
|
|
// make sure the directory exists
|
|
|
|
|
auto path = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
|
2026-02-22 12:28:56 +01:00
|
|
|
std::filesystem::create_directories(path.toStdString());
|
2023-06-12 15:26:31 +02:00
|
|
|
m_configFile = path + '/' + "modules.conf";
|
|
|
|
|
}
|
2023-06-11 19:06:16 +02:00
|
|
|
|
2023-06-13 13:02:38 +02:00
|
|
|
#ifdef TARGET_OS_Android
|
|
|
|
|
auto guiApp = qobject_cast<QGuiApplication*>(QCoreApplication::instance());
|
|
|
|
|
assert(guiApp);
|
|
|
|
|
connect(guiApp, &QGuiApplication::applicationStateChanged, this, [=](Qt::ApplicationState state) {
|
|
|
|
|
if (state == Qt::ApplicationInactive || state == Qt::ApplicationSuspended)
|
|
|
|
|
save();
|
|
|
|
|
});
|
|
|
|
|
#endif
|
|
|
|
|
|
2023-06-12 17:11:29 +02:00
|
|
|
extern void load_all_modules(ModuleManager*);
|
2023-06-12 15:26:31 +02:00
|
|
|
// the load_all_modules method is generated by cmake in the modules builddir.
|
2023-06-12 17:11:29 +02:00
|
|
|
// it essentially makes each module call 'load' on this class.
|
|
|
|
|
load_all_modules(this);
|
2023-06-12 15:26:31 +02:00
|
|
|
|
2023-06-13 13:02:38 +02:00
|
|
|
// connect to signals of the sections so we tell the QML to get the new
|
|
|
|
|
// filtered list on the user enabling / disabling something.
|
2024-10-08 20:46:26 +02:00
|
|
|
for (const auto *m : std::as_const(m_modules)) {
|
2023-06-12 15:26:31 +02:00
|
|
|
for (auto *s : m->sections()) {
|
2023-06-13 13:02:38 +02:00
|
|
|
connect (s, &ModuleSection::enabledChanged, this, [=]() {
|
|
|
|
|
switch (s->type()) {
|
2024-12-22 15:02:01 +01:00
|
|
|
case ModuleSection::SendMethod:
|
2023-06-13 13:02:38 +02:00
|
|
|
emit sendMenuSectionsChanged();
|
2023-07-04 21:54:18 +02:00
|
|
|
case ModuleSection::MainMenuItem:
|
|
|
|
|
emit mainMenuSectionsChanged();
|
2024-12-22 17:23:27 +01:00
|
|
|
case ModuleSection::OtherSectionType:
|
|
|
|
|
emit exploreTabItemsChanged();
|
2023-06-13 13:02:38 +02:00
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-06-12 15:26:31 +02:00
|
|
|
}
|
|
|
|
|
}
|
2023-06-11 19:06:16 +02:00
|
|
|
}
|
|
|
|
|
|
2023-06-13 13:02:38 +02:00
|
|
|
ModuleManager::~ModuleManager()
|
|
|
|
|
{
|
|
|
|
|
save();
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 00:18:14 +01:00
|
|
|
void ModuleManager::setFrontEnd(FrontEnd fe)
|
|
|
|
|
{
|
|
|
|
|
g_moduleManager_FrontEnd = fe;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ModuleManager::FrontEnd ModuleManager::frontEnd()
|
|
|
|
|
{
|
|
|
|
|
return g_moduleManager_FrontEnd;
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-12 17:11:29 +02:00
|
|
|
void ModuleManager::load(const char *translationUnit, const std::function<ModuleInfo *()> &function)
|
2023-06-11 19:06:16 +02:00
|
|
|
{
|
2023-06-12 17:11:29 +02:00
|
|
|
if (translationUnit) {
|
|
|
|
|
QString translations = QString::fromUtf8(translationUnit);
|
2023-06-12 10:46:54 +02:00
|
|
|
auto *translator = new QTranslator(this);
|
|
|
|
|
/*
|
|
|
|
|
* We try to load it from the default location as used in the translations subdir (:/i18n)
|
|
|
|
|
* and if that fails we try again at the root level of the QRC because individual modules
|
|
|
|
|
* can then just use one data.qrc and include translations, if they manage their own.
|
|
|
|
|
*/
|
|
|
|
|
if (translator->load(QLocale(), translations, QLatin1String("_"), QLatin1String(":/i18n")))
|
|
|
|
|
QCoreApplication::installTranslator(translator);
|
|
|
|
|
else if (translator->load(QLocale(), translations, QLatin1String("_"), QLatin1String(":")))
|
|
|
|
|
QCoreApplication::installTranslator(translator);
|
|
|
|
|
else
|
|
|
|
|
delete translator;
|
|
|
|
|
}
|
2026-01-19 21:42:35 +01:00
|
|
|
auto *info = function();
|
|
|
|
|
if (info == nullptr)
|
|
|
|
|
return;
|
|
|
|
|
if (info->enabled()) {
|
|
|
|
|
logCritical() << "ModuleInfo starts 'enabled', denying user choice. Cowerdly refusing to register it";
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-06-12 17:11:29 +02:00
|
|
|
info->setParent(this); // take ownership
|
2025-02-25 17:04:13 +01:00
|
|
|
int insertPoint = m_modules.size();
|
|
|
|
|
while (insertPoint > 0) {
|
|
|
|
|
if (m_modules.at(insertPoint - 1)->priority() >= info->priority())
|
|
|
|
|
break;
|
|
|
|
|
--insertPoint;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_modules.insert(insertPoint, info);
|
2025-10-20 21:50:50 +02:00
|
|
|
|
|
|
|
|
connect (info, &ModuleInfo::requestSaveSettings, this, [=]() {
|
|
|
|
|
QTimer::singleShot(2000, this, &ModuleManager::save);
|
|
|
|
|
});
|
2023-06-11 19:06:16 +02:00
|
|
|
}
|
2023-06-12 15:26:31 +02:00
|
|
|
|
2026-02-12 16:39:42 +01:00
|
|
|
void ModuleManager::loadConfiguration()
|
2023-06-12 15:26:31 +02:00
|
|
|
{
|
2023-06-13 13:02:38 +02:00
|
|
|
std::ifstream in(m_configFile.toStdString());
|
|
|
|
|
if (!in.is_open())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
auto dataSize = boost::filesystem::file_size(m_configFile.toStdString());
|
|
|
|
|
Streaming::BufferPool pool(dataSize);
|
|
|
|
|
in.read(pool.data(), dataSize);
|
|
|
|
|
Streaming::MessageParser parser(pool.commit(dataSize));
|
2024-10-24 22:09:40 +02:00
|
|
|
ModuleInfo *info = nullptr;
|
2023-06-13 13:02:38 +02:00
|
|
|
int type = -1;
|
2025-10-12 23:09:39 +02:00
|
|
|
bool prevEnabledCalled = false; // make sure we call those regardless of what the save-file contains
|
|
|
|
|
bool prevSettingsLoaded = false;
|
2023-06-13 13:02:38 +02:00
|
|
|
while (parser.next() == Streaming::FoundTag) {
|
|
|
|
|
if (parser.tag() == ModuleId) {
|
2025-10-12 23:09:39 +02:00
|
|
|
if (info && !prevEnabledCalled)
|
|
|
|
|
info->setEnabled(false);
|
|
|
|
|
if (info && !prevSettingsLoaded)
|
|
|
|
|
info->loadSettings(Streaming::ConstBuffer());
|
2023-06-13 13:02:38 +02:00
|
|
|
info = nullptr;
|
2025-10-12 23:09:39 +02:00
|
|
|
prevEnabledCalled = false;
|
|
|
|
|
prevSettingsLoaded = false;
|
2023-06-13 13:02:38 +02:00
|
|
|
type = -1;
|
2025-10-12 23:09:39 +02:00
|
|
|
const QString wantedId = QString::fromUtf8(parser.stringData());
|
2024-10-24 22:09:40 +02:00
|
|
|
for (auto *module : std::as_const(m_modules)) {
|
2023-06-13 13:02:38 +02:00
|
|
|
if (module->id() == wantedId) {
|
|
|
|
|
info = module;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-24 22:09:40 +02:00
|
|
|
// legacy support
|
|
|
|
|
else if (parser.tag() == LegacyModuleSectionId) {
|
2023-06-13 13:02:38 +02:00
|
|
|
type = parser.intData();
|
|
|
|
|
}
|
2024-10-24 22:09:40 +02:00
|
|
|
// legacy support
|
|
|
|
|
else if (parser.tag() == LegacyModuleSectionEnabled) {
|
2023-06-14 21:57:49 +02:00
|
|
|
if (info) {
|
2024-10-24 22:09:40 +02:00
|
|
|
info->setEnabled(info->enabled() || parser.boolData());
|
2023-06-14 21:57:49 +02:00
|
|
|
for (auto *s : info->sections()) {
|
|
|
|
|
if (s->type() == type) {
|
|
|
|
|
s->setEnabled(parser.boolData());
|
|
|
|
|
break;
|
|
|
|
|
}
|
2023-06-13 13:02:38 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-24 22:09:40 +02:00
|
|
|
else if (parser.tag() == ModuleEnabled) {
|
|
|
|
|
if (info)
|
|
|
|
|
info->setEnabled(parser.boolData());
|
2025-10-12 23:09:39 +02:00
|
|
|
prevEnabledCalled = true;
|
2024-10-24 22:09:40 +02:00
|
|
|
}
|
|
|
|
|
else if (parser.tag() == ModuleSaveData) {
|
|
|
|
|
if (info)
|
|
|
|
|
info->loadSettings(parser.bytesDataBuffer());
|
2025-10-12 23:09:39 +02:00
|
|
|
prevSettingsLoaded = true;
|
2024-10-24 22:09:40 +02:00
|
|
|
}
|
2023-06-13 13:02:38 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ModuleManager::save() const
|
|
|
|
|
{
|
2024-01-30 17:22:12 +01:00
|
|
|
if (m_modules.isEmpty()) {
|
|
|
|
|
// be nice to the devs that use both mobile and non-mobile on their
|
|
|
|
|
// account, the non-mobile has no modules, as such we just don't
|
|
|
|
|
// (over)write the config file.
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-06-13 13:02:38 +02:00
|
|
|
int saveFileSize = 100;
|
2026-01-14 17:37:47 +01:00
|
|
|
auto pool = std::make_shared<Streaming::BufferPool>(100000);
|
2024-10-24 22:09:40 +02:00
|
|
|
QList<Streaming::ConstBuffer> saveData;
|
2023-06-13 13:02:38 +02:00
|
|
|
for (const auto *m : m_modules) {
|
2024-10-24 22:09:40 +02:00
|
|
|
saveFileSize += m->id().size() * 3 + 3;
|
|
|
|
|
saveData.append(m->saveSettings(pool));
|
|
|
|
|
saveFileSize += saveData.back().size() + 12;
|
2023-06-13 13:02:38 +02:00
|
|
|
}
|
|
|
|
|
|
2024-10-24 22:09:40 +02:00
|
|
|
pool->reserve(saveFileSize);
|
2023-06-13 13:02:38 +02:00
|
|
|
Streaming::MessageBuilder builder(pool);
|
2024-10-24 22:09:40 +02:00
|
|
|
for (int i = 0; i < m_modules.size(); ++i) {
|
|
|
|
|
const auto *m = m_modules.at(i);
|
2023-06-13 13:02:38 +02:00
|
|
|
builder.add(ModuleId, m->id().toStdString());
|
2024-10-24 22:09:40 +02:00
|
|
|
if (m->enabled())
|
|
|
|
|
builder.add(ModuleEnabled, true);
|
|
|
|
|
auto saveFile = saveData.at(i);
|
|
|
|
|
if (!saveFile.isEmpty())
|
|
|
|
|
builder.add(ModuleSaveData, saveFile);
|
2023-06-13 13:02:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto data = builder.buffer();
|
|
|
|
|
|
|
|
|
|
// hash the new file and check if its different lest we can skip saving
|
|
|
|
|
QFile origFile(m_configFile);
|
|
|
|
|
if (origFile.open(QIODevice::ReadOnly)) {
|
|
|
|
|
CRIPEMD160 fileHasher;
|
|
|
|
|
auto origContent = origFile.readAll();
|
|
|
|
|
fileHasher.write(origContent.data(), origContent.size());
|
|
|
|
|
char fileHash[CRIPEMD160::OUTPUT_SIZE];
|
|
|
|
|
fileHasher.finalize(fileHash);
|
|
|
|
|
|
|
|
|
|
CRIPEMD160 memHasher;
|
|
|
|
|
memHasher.write(data.begin(), data.size());
|
|
|
|
|
char memHash[CRIPEMD160::OUTPUT_SIZE];
|
|
|
|
|
memHasher.finalize(memHash);
|
|
|
|
|
if (memcmp(fileHash, memHash, CRIPEMD160::OUTPUT_SIZE) == 0) {
|
|
|
|
|
// no changes, so don't write.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
auto configFile = m_configFile.toStdString();
|
|
|
|
|
auto configFileNew = configFile + "~";
|
|
|
|
|
std::ofstream outFile(configFileNew);
|
|
|
|
|
outFile.write(data.begin(), data.size());
|
|
|
|
|
outFile.flush();
|
|
|
|
|
outFile.close();
|
2023-06-18 15:41:57 +02:00
|
|
|
std::filesystem::rename(configFileNew, configFile);
|
2023-06-13 13:02:38 +02:00
|
|
|
} catch (const std::exception &e) {
|
|
|
|
|
logFatal() << "Failed to save the wallet.dat. Reason:" << e;
|
|
|
|
|
}
|
2023-06-12 15:26:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QList<ModuleInfo *> ModuleManager::registeredModules() const
|
|
|
|
|
{
|
|
|
|
|
return m_modules;
|
|
|
|
|
}
|
2023-06-13 13:02:38 +02:00
|
|
|
|
2025-10-31 17:24:01 +01:00
|
|
|
bool ModuleManager::isModuleAvailable(const QString &id) const
|
|
|
|
|
{
|
|
|
|
|
for (const auto *m : m_modules) {
|
|
|
|
|
if (m->id() == id)
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-13 13:02:38 +02:00
|
|
|
QList<ModuleSection *> ModuleManager::sendMenuSections() const
|
|
|
|
|
{
|
|
|
|
|
QList<ModuleSection *> answer;
|
|
|
|
|
for (const auto *m : m_modules) {
|
|
|
|
|
for (auto *s : m->sections()) {
|
2024-12-22 15:02:01 +01:00
|
|
|
if (s->enabled() && s->type() == ModuleSection::SendMethod)
|
2023-06-13 13:02:38 +02:00
|
|
|
answer.append(s);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return answer;
|
|
|
|
|
}
|
2023-07-04 21:54:18 +02:00
|
|
|
|
2024-10-08 22:04:23 +02:00
|
|
|
QList<ModuleSection *> ModuleManager::mainMenuSections() const
|
2023-07-04 21:54:18 +02:00
|
|
|
{
|
|
|
|
|
QList<ModuleSection *> answer;
|
|
|
|
|
for (const auto *m : m_modules) {
|
|
|
|
|
for (auto *s : m->sections()) {
|
|
|
|
|
if (s->enabled() && s->type() == ModuleSection::MainMenuItem)
|
|
|
|
|
answer.append(s);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return answer;
|
|
|
|
|
}
|
2024-05-31 21:51:57 +02:00
|
|
|
|
2024-12-22 17:23:27 +01:00
|
|
|
QList<ModuleSection*> ModuleManager::exploreTabItems() const
|
|
|
|
|
{
|
|
|
|
|
QList<ModuleSection *> answer;
|
|
|
|
|
for (const auto *m : m_modules) {
|
|
|
|
|
for (auto *s : m->sections()) {
|
2024-12-30 15:59:01 +01:00
|
|
|
if (s->type() == ModuleSection::OtherSectionTypeDefault
|
|
|
|
|
|| (s->enabled() && s->type() == ModuleSection::OtherSectionType))
|
2024-12-22 17:23:27 +01:00
|
|
|
answer.append(s);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return answer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QList<ModuleSection*> ModuleManager::oftenUsedItems() const
|
|
|
|
|
{
|
|
|
|
|
QList<ModuleSection *> answer;
|
|
|
|
|
// TODO
|
|
|
|
|
return answer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2024-05-31 21:51:57 +02:00
|
|
|
ModuleSection *ModuleManager::sectionOnPlugin(const QString &pluginId, const QString §ionId) const
|
|
|
|
|
{
|
|
|
|
|
for (const auto *m : m_modules) {
|
|
|
|
|
if (m->id() == pluginId) {
|
|
|
|
|
for (auto *s : m->sections()) {
|
|
|
|
|
if (s->type() == ModuleSection::CustomSectionType
|
|
|
|
|
&& s->sectionId() == sectionId)
|
|
|
|
|
return s;
|
|
|
|
|
}
|
2024-10-25 11:33:11 +02:00
|
|
|
break;
|
2024-05-31 21:51:57 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
2024-10-25 11:33:11 +02:00
|
|
|
|
2025-10-31 17:24:01 +01:00
|
|
|
ModuleInfo *ModuleManager::moduleInfo(const QString &pluginId) const
|
|
|
|
|
{
|
|
|
|
|
for (auto *m : m_modules) {
|
|
|
|
|
if (m->id() == pluginId) {
|
|
|
|
|
return m;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-01 17:17:57 +01:00
|
|
|
ModuleSection *ModuleManager::moduleSection(const QString &pluginId) const
|
|
|
|
|
{
|
|
|
|
|
for (const auto *m : m_modules) {
|
|
|
|
|
if (m->id() == pluginId) {
|
|
|
|
|
if (m->sections().empty())
|
|
|
|
|
return nullptr;
|
|
|
|
|
for (auto *s : m->sections()) {
|
|
|
|
|
if (s->type() == ModuleSection::OtherSectionType)
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
return m->sections().first();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-25 11:33:11 +02:00
|
|
|
void ModuleManager::allSections(const std::function<void (const QString &, ModuleSection *)> &handler)
|
|
|
|
|
{
|
|
|
|
|
for (const auto *m : std::as_const(m_modules)) {
|
|
|
|
|
for (auto *s : m->sections()) {
|
|
|
|
|
handler(m->id(), s);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-27 18:39:43 +01:00
|
|
|
|
|
|
|
|
void ModuleManager::requestAddPage(const QString &url, QObject *dataObject)
|
|
|
|
|
{
|
|
|
|
|
emit addPage(url, dataObject);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ModuleManager::markChild(QObject *parent, QObject *child)
|
|
|
|
|
{
|
|
|
|
|
if (parent && child)
|
|
|
|
|
child->setParent(parent);
|
|
|
|
|
}
|