Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions custom-example/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ qt_add_resources(${CMAKE_PROJECT_NAME} custom_json
FILES "${CUSTOM_JSON_RESOURCE}"
)

# CustomSettings group metadata: SettingsGroup loads it from :/json at runtime and
# the settings QML generator reads it from src/Settings at configure time.
set(CUSTOM_SETTINGS_JSON_RESOURCE "${CMAKE_CURRENT_SOURCE_DIR}/src/Settings/Custom.SettingsGroup.json")
qgc_set_qt_resource_alias("${CUSTOM_SETTINGS_JSON_RESOURCE}")
qt_add_resources(${CMAKE_PROJECT_NAME} custom_settings_json
PREFIX "/json"
FILES "${CUSTOM_SETTINGS_JSON_RESOURCE}"
)

list(APPEND QML_IMPORT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/res")
set(QML_IMPORT_PATH "${QML_IMPORT_PATH}" CACHE STRING "Extra QML import paths" FORCE)

Expand Down Expand Up @@ -200,6 +209,8 @@ set(CUSTOM_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/MissionManager/PerimeterScanComplexItem.h
${CMAKE_CURRENT_SOURCE_DIR}/src/MissionManager/PerimeterScanPlanCreator.cc
${CMAKE_CURRENT_SOURCE_DIR}/src/MissionManager/PerimeterScanPlanCreator.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Settings/CustomSettings.cc
${CMAKE_CURRENT_SOURCE_DIR}/src/Settings/CustomSettings.h
PARENT_SCOPE
)

Expand All @@ -218,6 +229,7 @@ set(CUSTOM_INCLUDE_DIRECTORIES
${CMAKE_CURRENT_SOURCE_DIR}/src/AutoPilotPlugin
${CMAKE_CURRENT_SOURCE_DIR}/src/FirmwarePlugin
${CMAKE_CURRENT_SOURCE_DIR}/src/MissionManager
${CMAKE_CURRENT_SOURCE_DIR}/src/Settings
PARENT_SCOPE
)

Expand Down
20 changes: 20 additions & 0 deletions custom-example/src/AppSettings/pages/Custom.SettingsUI.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": 1,
"fileType": "SettingsUI",
"groups": [
{
"heading": "Custom Example",
"controls": [
{
"setting": "customSettings.showAttitudeWidget"
},
{
"setting": "customSettings.updateInterval"
},
{
"setting": "customSettings.operatorName"
}
]
}
]
}
14 changes: 14 additions & 0 deletions custom-example/src/AppSettings/pages/SettingsPages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": 1,
"fileType": "SettingsPages",
"comment": "Custom-build overlay merged into src/AppSettings/pages/SettingsPages.json. Entries can append, insertAfter/insertBefore, replace (same name), or remove pages.",
"pages": [
{
"name": "Custom",
"qml": "CustomSettings.qml",
"icon": "qrc:/res/gear-white.svg",
"pageDefinition": "Custom.SettingsUI.json",
"insertAfter": "General"
}
]
}
16 changes: 4 additions & 12 deletions custom-example/src/CustomPlugin.cc
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#include "CustomPlugin.h"
#include "CustomSettings.h"
#include "PerimeterScanComplexItem.h"
#include "PerimeterScanPlanCreator.h"
#include "QmlComponentInfo.h"
#include "QGCLoggingCategory.h"
#include "QGCPalette.h"
#include "QGCMAVLink.h"
#include "AppSettings.h"
#include "SettingsManager.h"

#include <QtCore/QApplicationStatic>
#include <QtQml/QQmlApplicationEngine>
Expand Down Expand Up @@ -52,18 +53,9 @@ void CustomPlugin::_advancedChanged(bool changed)
emit _options->showFirmwareUpgradeChanged(changed);
}

void CustomPlugin::_addSettingsEntry(const QString &title, const char *qmlFile, const char *iconFile)
void CustomPlugin::registerCustomSettings(SettingsManager *settingsManager)
{
Q_CHECK_PTR(qmlFile);
// 'this' instance will take ownership on the QmlComponentInfo instance
_customSettingsList.append(QVariant::fromValue(
new QmlComponentInfo(
title,
QUrl::fromUserInput(qmlFile),
!iconFile ? QUrl() : QUrl::fromUserInput(iconFile),
this)
)
);
settingsManager->registerCustomSettingsGroup(QStringLiteral("customSettings"), new CustomSettings());
}

void CustomPlugin::adjustSettingMetaData(const QString& settingsGroup, FactMetaData& metaData, bool &userVisible)
Expand Down
6 changes: 2 additions & 4 deletions custom-example/src/CustomPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ class PlanCreator;

class CustomOptions;
class CustomPlugin;
class CustomSettings;
class QQmlApplicationEngine;

Q_DECLARE_LOGGING_CATEGORY(CustomLog)
Expand Down Expand Up @@ -84,17 +83,16 @@ class CustomPlugin : public QGCCorePlugin
const QString &kmlOrShpFile = QString()) final;
/// Adds the Perimeter Scan plan creator to the New Plan dialog.
QList<PlanCreator *> planCreators(PlanMasterController *planMasterController) final;
/// Registers the CustomSettings group so the generated Custom settings page can access it.
void registerCustomSettings(SettingsManager *settingsManager) final;

private slots:
void _advancedChanged(bool advanced);

private:
void _addSettingsEntry(const QString& title, const char* qmlFile, const char* iconFile = nullptr);

CustomOptions *_options = nullptr;
QQmlApplicationEngine *_qmlEngine = nullptr;
class CustomOverrideInterceptor *_urlInterceptor = nullptr;
QVariantList _customSettingsList; // Not to be mixed up with QGCCorePlugin implementation
};

/*===========================================================================*/
Expand Down
29 changes: 29 additions & 0 deletions custom-example/src/Settings/Custom.SettingsGroup.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"version": 1,
"fileType": "FactMetaData",
"QGC.MetaData.Facts": [
{
"name": "showAttitudeWidget",
"shortDesc": "Show the custom attitude widget in the fly view.",
"type": "bool",
"default": true,
"label": "Show attitude widget"
},
{
"name": "updateInterval",
"shortDesc": "How often the custom widgets refresh their values.",
"type": "uint32",
"enumStrings": "Slow,Normal,Fast",
"enumValues": "0,1,2",
"default": 1,
"label": "Widget update rate"
},
{
"name": "operatorName",
"shortDesc": "Operator name shown in the custom fly view overlay.",
"type": "string",
"default": "",
"label": "Operator name"
}
]
}
9 changes: 9 additions & 0 deletions custom-example/src/Settings/CustomSettings.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#include "CustomSettings.h"

DECLARE_SETTINGGROUP(Custom, "Custom")
{
}

DECLARE_SETTINGSFACT(CustomSettings, showAttitudeWidget)
DECLARE_SETTINGSFACT(CustomSettings, updateInterval)
DECLARE_SETTINGSFACT(CustomSettings, operatorName)
23 changes: 23 additions & 0 deletions custom-example/src/Settings/CustomSettings.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once

#include <QtQmlIntegration/QtQmlIntegration>

#include "SettingsGroup.h"

/// Example custom-build settings group. Registered at runtime via
/// CustomPlugin::registerCustomSettings so generated settings pages can reference
/// facts as QGroundControl.settingsManager.customSettings.<factName>.
class CustomSettings : public SettingsGroup
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
CustomSettings(QObject* parent = nullptr);

DEFINE_SETTING_NAME_GROUP()

DEFINE_SETTINGFACT(showAttitudeWidget)
DEFINE_SETTINGFACT(updateInterval)
DEFINE_SETTINGFACT(operatorName)
};
18 changes: 18 additions & 0 deletions docs/en/qgc-dev-guide/views/settings_generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,24 @@ Search terms are derived from:
- Add your new QML filename to `_generated_qml_names`.
4. Build QGC to generate and include the new page.

## Custom Build Settings Pages

Custom builds (`QGC_CUSTOM_DIR`) can add, replace, reposition, or remove generated settings pages without overriding the stock generated QML:

1. **Page list overlay** — create `<custom>/src/AppSettings/pages/SettingsPages.json`. Its entries are merged into the stock page list at configure time:
- An entry whose `name` matches a stock page **replaces** it in place.
- New entries support `insertAfter`/`insertBefore` (referencing a stock page `name`); otherwise they append.
- `{ "remove": "<name>" }` removes a stock page.
2. **Page definitions** — put `*.SettingsUI.json` files in the same custom pages dir. A file with the same name as a stock definition shadows it.
3. **Custom settings groups** — to reference facts that don't exist in stock QGC:
- Add `<custom>/src/Settings/<Name>.SettingsGroup.json` fact metadata (also compile it into the app under the `:/json` resource prefix).
- Create a `SettingsGroup` subclass for it.
- Override `QGCCorePlugin::registerCustomSettings` and call `SettingsManager::registerCustomSettingsGroup("<accessor>", new MySettings())` (the manager takes ownership). The accessor must be the camelCase JSON stem plus `Settings` (e.g. `Custom.SettingsGroup.json` → `customSettings`) so generated pages resolve `QGroundControl.settingsManager.<accessor>.<fact>`.

CMake wires this automatically when the custom directories exist; the generated output list is computed by the generator's `--list-outputs` mode.

The `custom-example` build in the repo includes a complete working example of all of the above: a page list overlay adding a custom settings page, its page definition, the custom settings group (fact metadata and `SettingsGroup` subclass), and the plugin registration override.

## Important Notes

- If a page in `SettingsPages.json` has no `pageDefinition`, it is treated as hand-written QML/URL content and not generated.
Expand Down
6 changes: 6 additions & 0 deletions src/API/QGCCorePlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class QGeoPositionInfoSource;
class QmlObjectListModel;
class QQmlApplicationEngine;
class QQuickItem;
class SettingsManager;
class Vehicle;
class VideoReceiver;
class VideoSink;
Expand Down Expand Up @@ -84,6 +85,11 @@ class QGCCorePlugin : public QObject
/// If not overridden, metaData and userVisible are left unchanged.
virtual void adjustSettingMetaData(const QString &settingsGroup, FactMetaData &metaData, bool &userVisible);

/// Called at the end of SettingsManager::init. Override to register custom build
/// settings groups via SettingsManager::registerCustomSettingsGroup so generated
/// settings pages can reference them as QGroundControl.settingsManager.<accessor>.
virtual void registerCustomSettings(SettingsManager *settingsManager) { Q_UNUSED(settingsManager); }

/// @return The message to show to the user when they are prompted to confirm turning on advanced ui.
virtual QString showAdvancedUIMessage() const;

Expand Down
76 changes: 59 additions & 17 deletions src/AppSettings/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,71 @@ file(GLOB _generator_sources CONFIGURE_DEPENDS
file(GLOB _page_definitions CONFIGURE_DEPENDS "${SETTINGS_PAGES_DIR}/*.json")
file(GLOB _settings_metadata CONFIGURE_DEPENDS "${SETTINGS_METADATA_DIR}/*.SettingsGroup.json")

# Generated QML outputs (must match what generate_pages.py produces)
set(_generated_qml_names
ADSBServerSettings.qml
CommLinksSettings.qml
FlyViewSettings.qml
GeneralSettings.qml
LoggingSettings.qml
MapsSettings.qml
NTRIPSettings.qml
PlanViewSettings.qml
PX4LogTransferSettings.qml
RemoteIDSettings.qml
SettingsPagesModel.qml
TelemetrySettings.qml
VideoSettings.qml
Viewer3DSettings.qml
)
# Custom builds may overlay the page list (SettingsPages.json), add/shadow page
# definitions, and add custom SettingsGroup.json fact metadata.
set(_custom_codegen_args "")
set(CUSTOM_SETTINGS_PAGES_DIR "${CMAKE_SOURCE_DIR}/${QGC_CUSTOM_DIR}/src/AppSettings/pages")
set(CUSTOM_SETTINGS_METADATA_DIR "${CMAKE_SOURCE_DIR}/${QGC_CUSTOM_DIR}/src/Settings")
if(QGC_CUSTOM_BUILD AND IS_DIRECTORY "${CUSTOM_SETTINGS_PAGES_DIR}")
list(APPEND _custom_codegen_args --custom-pages-dir "${CUSTOM_SETTINGS_PAGES_DIR}")
file(GLOB _custom_page_definitions CONFIGURE_DEPENDS "${CUSTOM_SETTINGS_PAGES_DIR}/*.json")
list(APPEND _page_definitions ${_custom_page_definitions})
endif()
if(QGC_CUSTOM_BUILD AND IS_DIRECTORY "${CUSTOM_SETTINGS_METADATA_DIR}")
list(APPEND _custom_codegen_args --custom-settings-dir "${CUSTOM_SETTINGS_METADATA_DIR}")
file(GLOB _custom_settings_metadata CONFIGURE_DEPENDS "${CUSTOM_SETTINGS_METADATA_DIR}/*.SettingsGroup.json")
list(APPEND _settings_metadata ${_custom_settings_metadata})
endif()

if(_custom_codegen_args)
# The overlay can add/remove/replace pages, so ask the generator for the
# resulting output file list instead of hardcoding it.
execute_process(
COMMAND ${Python3_EXECUTABLE} -m tools.generators.settings_qml.generate_pages
--list-outputs ${_custom_codegen_args}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
OUTPUT_VARIABLE _generated_qml_names
RESULT_VARIABLE _list_outputs_result
ERROR_VARIABLE _list_outputs_error
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(NOT _list_outputs_result EQUAL 0)
message(FATAL_ERROR "Settings QML generator --list-outputs failed:\n${_list_outputs_error}")
endif()
# Python emits CRLF on Windows; execute_process does not normalize it
string(REPLACE "\r" "" _generated_qml_names "${_generated_qml_names}")
string(REPLACE "\n" ";" _generated_qml_names "${_generated_qml_names}")
else()
# Generated QML outputs (must match what generate_pages.py produces)
set(_generated_qml_names
ADSBServerSettings.qml
CommLinksSettings.qml
FlyViewSettings.qml
GeneralSettings.qml
LoggingSettings.qml
MapsSettings.qml
NTRIPSettings.qml
PlanViewSettings.qml
PX4LogTransferSettings.qml
RemoteIDSettings.qml
SettingsPagesModel.qml
TelemetrySettings.qml
VideoSettings.qml
Viewer3DSettings.qml
)
endif()

set(_custom_extra_args_keyword "")
if(_custom_codegen_args)
set(_custom_extra_args_keyword EXTRA_ARGS ${_custom_codegen_args})
endif()

qgc_add_qml_codegen(GenerateSettingsQml
GENERATE_AT_CONFIGURE
GENERATOR_MODULE tools.generators.settings_qml.generate_pages
OUTPUT_DIR "${SETTINGS_QML_GEN_DIR}"
QML_NAMES ${_generated_qml_names}
${_custom_extra_args_keyword}
DEPENDS ${_generator_sources} ${_page_definitions} ${_settings_metadata}
COMMENT "Generating QML settings pages from JSON definitions"
)
Expand Down
34 changes: 33 additions & 1 deletion src/Settings/SettingsManager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,17 @@
#include "Viewer3DSettings.h"
#include "JsonParsing.h"
#include "QGCCorePlugin.h"
#include "SettingsGroup.h"

#include <QtCore/QApplicationStatic>
#include <QtCore/QRegularExpression>

QGC_LOGGING_CATEGORY(SettingsManagerLog, "Utilities.SettingsManager")

Q_APPLICATION_STATIC(SettingsManager, _settingsManagerInstance);

SettingsManager::SettingsManager(QObject *parent)
: QObject(parent)
: QQmlPropertyMap(this, parent)
{
qCDebug(SettingsManagerLog) << this;
}
Expand Down Expand Up @@ -79,6 +81,36 @@ void SettingsManager::init()
_viewer3DSettings = new Viewer3DSettings(this);
_adsbVehicleManagerSettings = new ADSBVehicleManagerSettings(this);
_apmMavlinkStreamRateSettings = new APMMavlinkStreamRateSettings(this);

QGCCorePlugin::instance()->registerCustomSettings(this);
}

void SettingsManager::registerCustomSettingsGroup(const QString &accessorName, SettingsGroup *group)
{
// Must be a valid QML identifier or generated pages can't resolve the group via dot notation
static const QRegularExpression validAccessorRe(QStringLiteral("^[a-z_][A-Za-z0-9_]*$"));
if (!validAccessorRe.match(accessorName).hasMatch() || !group) {
qCWarning(SettingsManagerLog) << "registerCustomSettingsGroup: invalid accessor name or null group" << accessorName;
delete group;
return;
}
if (contains(accessorName)) {
qCWarning(SettingsManagerLog) << "registerCustomSettingsGroup: accessor already registered" << accessorName;
// Re-registering the stored group itself must not destroy it
if (group != value(accessorName).value<QObject*>()) {
delete group;
}
return;
}
if (staticMetaObject.indexOfProperty(accessorName.toUtf8().constData()) != -1) {
qCWarning(SettingsManagerLog) << "registerCustomSettingsGroup: accessor collides with a built-in settings group" << accessorName;
delete group;
return;
}

group->setParent(this);
insert(accessorName, QVariant::fromValue<QObject*>(group));
qCDebug(SettingsManagerLog) << "Registered custom settings group" << accessorName;
}

ADSBVehicleManagerSettings *SettingsManager::adsbVehicleManagerSettings() const { return _adsbVehicleManagerSettings; }
Expand Down
Loading
Loading