551 lines
18 KiB
C++
551 lines
18 KiB
C++
#include "config.h"
|
|
|
|
#include <Viewer/GraphicsPresets.hxx>
|
|
|
|
// std
|
|
#include <unordered_set>
|
|
|
|
// SG
|
|
#include <simgear/io/iostreams/sgstream.hxx>
|
|
#include <simgear/misc/sg_dir.hxx>
|
|
#include <simgear/props/props_io.hxx>
|
|
#include <simgear/structure/commands.hxx>
|
|
#include <simgear/structure/exception.hxx>
|
|
|
|
// FG
|
|
#include <Main/fg_props.hxx>
|
|
#include <Main/globals.hxx>
|
|
#include <Main/locale.hxx>
|
|
#include <Main/sentryIntegration.hxx>
|
|
#include <Scenery/scenery.hxx>
|
|
|
|
using namespace std;
|
|
|
|
namespace {
|
|
|
|
const char* kPresetPropPath = "/sim/rendering/preset";
|
|
const char* kPresetNameProp = "/sim/rendering/preset-name";
|
|
const char* kPresetDescriptionProp = "/sim/rendering/preset-description";
|
|
const char* kPresetActiveProp = "/sim/rendering/preset-active";
|
|
|
|
|
|
const char* kRestartRequiredProp = "/sim/rendering/restart-required";
|
|
const char* kSceneryReloadRequiredProp = "/sim/rendering/scenery-reload-required";
|
|
const char* kCompositorReloadRequiredProp = "/sim/rendering/compositor-reload-required";
|
|
|
|
// define the property prefixes which graphics presets are allowed to
|
|
// modify. Changes to properties outside these prefixes will be
|
|
// blocked
|
|
|
|
const string_list kWitelistedPrefixes = {
|
|
"/sim/rendering"};
|
|
|
|
|
|
} // namespace
|
|
|
|
namespace flightgear {
|
|
|
|
class GraphicsPresets::GraphicsConfigChangeListener : public SGPropertyChangeListener
|
|
{
|
|
struct WatchedProp {
|
|
std::string previousValue;
|
|
SGPropertyNode_ptr node;
|
|
};
|
|
|
|
using PropertyNodeList = std::vector<WatchedProp>;
|
|
PropertyNodeList _watchedProps;
|
|
|
|
public:
|
|
GraphicsConfigChangeListener()
|
|
{
|
|
_presetProp = fgGetNode(kPresetPropPath, true);
|
|
}
|
|
|
|
void registerWithProperty(SGPropertyNode_ptr n)
|
|
{
|
|
auto it = findProp(n);
|
|
if (it != _watchedProps.end()) {
|
|
// this would happen if a preset somehow set the same property more than once
|
|
SG_LOG(SG_GUI, SG_ALERT, "GraphicsPresets: Duplicate registration for:" << n->getPath());
|
|
return;
|
|
}
|
|
|
|
WatchedProp p = {n->getStringValue(), n};
|
|
_watchedProps.push_back(p);
|
|
n->addChangeListener(this);
|
|
}
|
|
|
|
void unregisterFromProperties()
|
|
{
|
|
for (const auto& w : _watchedProps) {
|
|
w.node->removeChangeListener(this);
|
|
}
|
|
_watchedProps.clear();
|
|
}
|
|
|
|
void valueChanged(SGPropertyNode* prop) override
|
|
{
|
|
if (!_presetProp->hasValue()) {
|
|
return;
|
|
}
|
|
|
|
auto it = findProp(prop);
|
|
assert(it != _watchedProps.end());
|
|
const std::string newValue = prop->getStringValue();
|
|
if (newValue == it->previousValue) {
|
|
return;
|
|
}
|
|
|
|
SG_LOG(SG_GUI, SG_INFO, "GraphicsPreset clearing; setting:" << prop->getPath() << " was modified");
|
|
flightgear::addSentryBreadcrumb("clearing graphics preset, config was customised at:" + prop->getPath(), "info");
|
|
_presetProp->clearValue();
|
|
|
|
auto gp = globals->get_subsystem<GraphicsPresets>();
|
|
gp->clearPreset();
|
|
}
|
|
|
|
private:
|
|
SGPropertyNode_ptr _presetProp;
|
|
|
|
PropertyNodeList::iterator findProp(SGPropertyNode* node)
|
|
{
|
|
return std::find_if(_watchedProps.begin(), _watchedProps.end(), [node](const WatchedProp& w) {
|
|
return w.node == node;
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
@brief monitor a collection of properties, and set a flag property to true when any of them are
|
|
modified. Used to track the list of 'reload-required' and 'restart-requried' properties defined in
|
|
FG_DATA/Video/graphics-properties.xml
|
|
*/
|
|
class GraphicsPresets::RequiredPropertyListener : public SGPropertyChangeListener
|
|
{
|
|
public:
|
|
RequiredPropertyListener(const std::string& requiredProp, SGPropertyNode_ptr props)
|
|
{
|
|
_requiredProp = fgGetNode(requiredProp, true);
|
|
_requiredProp->setBoolValue(false);
|
|
|
|
// would happen if graphics-properties.xml was malformed
|
|
if (!props)
|
|
return;
|
|
|
|
for (const auto& c : props->getChildren("property")) {
|
|
// tolerate exterior whitespace in the XML
|
|
string path = simgear::strutils::strip(c->getStringValue());
|
|
if (path.empty())
|
|
continue;
|
|
|
|
SGPropertyNode_ptr n = fgGetNode(path, true);
|
|
if (n) {
|
|
n->addChangeListener(this);
|
|
}
|
|
} // of properties iteration
|
|
}
|
|
|
|
void clearRequiredFlag()
|
|
{
|
|
if (_requiredProp->getBoolValue()) {
|
|
_requiredProp->setBoolValue(false);
|
|
}
|
|
}
|
|
|
|
void valueChanged(SGPropertyNode* prop) override
|
|
{
|
|
if (!_requiredProp->getBoolValue()) {
|
|
SG_LOG(SG_GUI, SG_INFO, "GraphicsPreset: saw modification of:" << prop->getPath() << ", setting:" << _requiredProp->getPath() << " to true");
|
|
_requiredProp->setBoolValue(true);
|
|
}
|
|
}
|
|
|
|
private:
|
|
SGPropertyNode_ptr _requiredProp;
|
|
};
|
|
|
|
static bool do_apply_preset(const SGPropertyNode* arg, SGPropertyNode* root)
|
|
{
|
|
auto gp = globals->get_subsystem<GraphicsPresets>();
|
|
|
|
bool result = false;
|
|
if (arg->hasChild("path")) {
|
|
SGPath p = SGPath::fromUtf8(arg->getStringValue("path"));
|
|
if (!p.exists()) {
|
|
SG_LOG(SG_IO, SG_ALERT, "apply-graphics-preset: no file at:" << p);
|
|
return false;
|
|
}
|
|
|
|
result = gp->applyCustomPreset(p);
|
|
} else if (arg->hasChild("preset-name")) {
|
|
// helper for PUI UI: PUI ComboBox gives us the name, not the ID.
|
|
// so allow specify a preset by (localized) name.
|
|
gp->applyPresetByName(arg->getStringValue("preset-name"));
|
|
} else if (arg->hasChild("preset")) {
|
|
fgSetString(kPresetPropPath, arg->getStringValue("preset"));
|
|
result = gp->applyCurrentPreset();
|
|
} else {
|
|
// just apply the current selected one
|
|
result = gp->applyCurrentPreset();
|
|
}
|
|
|
|
if (arg->getBoolValue("reload-scenery")) {
|
|
if (fgGetBool(kSceneryReloadRequiredProp)) {
|
|
SG_LOG(SG_GUI, SG_MANDATORY_INFO, "apply-graphics-preset: triggering scenery reload");
|
|
globals->get_scenery()->reinit(); // this will set
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static bool do_save_preset(const SGPropertyNode* arg, SGPropertyNode* root)
|
|
{
|
|
if (!arg->hasChild("path")) {
|
|
SG_LOG(SG_GUI, SG_ALERT, "do_save_preset: no out path argument provided");
|
|
return false;
|
|
}
|
|
|
|
const string spath = arg->getStringValue("path");
|
|
if (spath == "!ask") {
|
|
}
|
|
|
|
const SGPath path = SGPath::fromUtf8(spath);
|
|
const string name = arg->getStringValue("name");
|
|
const string description = arg->getStringValue("description");
|
|
auto gp = globals->get_subsystem<GraphicsPresets>();
|
|
|
|
return gp->saveToXML(path, name, description);
|
|
}
|
|
|
|
static bool do_list_standard_presets(const SGPropertyNode* arg, SGPropertyNode* root)
|
|
{
|
|
auto gp = globals->get_subsystem<GraphicsPresets>();
|
|
if (!arg->hasValue("destination-path")) {
|
|
SG_LOG(SG_GUI, SG_ALERT, "list-graphics-preset: no destination path supplied");
|
|
return false;
|
|
}
|
|
|
|
SGPropertyNode_ptr destRoot = fgGetNode(arg->getStringValue("destination-path"), true /* create */);
|
|
|
|
if (arg->hasValue("clear-destination")) {
|
|
destRoot->removeAllChildren();
|
|
}
|
|
|
|
// format the way PUI combo-box (actualy, fgValueList) like it
|
|
if (arg->getBoolValue("as-combobox-values")) {
|
|
for (const auto& preset : gp->listPresets()) {
|
|
SGPropertyNode_ptr v = destRoot->addChild("value");
|
|
v->setStringValue(preset.name);
|
|
}
|
|
} else {
|
|
for (const auto& preset : gp->listPresets()) {
|
|
SGPropertyNode_ptr pn = destRoot->addChild("preset");
|
|
pn->setStringValue("name", preset.name);
|
|
pn->setStringValue("id", preset.id);
|
|
pn->setStringValue("description", preset.description);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
GraphicsPresets::GraphicsPresets()
|
|
{
|
|
// needs to be done early so that applyInitialPreset
|
|
// can setup the registration
|
|
_listener.reset(new GraphicsConfigChangeListener);
|
|
}
|
|
|
|
GraphicsPresets::~GraphicsPresets()
|
|
{
|
|
}
|
|
|
|
void GraphicsPresets::applyInitialPreset()
|
|
{
|
|
const string currentPreset = fgGetString(kPresetPropPath);
|
|
fgSetBool(kPresetActiveProp, false);
|
|
if (!currentPreset.empty()) {
|
|
SG_LOG(SG_GUI, SG_INFO, "Applying graphics preset:" << currentPreset);
|
|
addSentryBreadcrumb("Startup selection of preset:" + currentPreset, "info");
|
|
applyCurrentPreset();
|
|
}
|
|
}
|
|
|
|
void GraphicsPresets::init()
|
|
{
|
|
// create the change listeners. Because we do the initial application before this, we won't
|
|
// see any changes caused by any initial preset load (or autosave.xml load), only
|
|
// future changes made by the user via settings UI.
|
|
SGPropertyNode_ptr graphicsPropsXML(new SGPropertyNode);
|
|
try {
|
|
readProperties(globals->findDataPath("Video/graphics-properties.xml"), graphicsPropsXML.get());
|
|
|
|
_restartListener.reset(new RequiredPropertyListener{kRestartRequiredProp, graphicsPropsXML->getChild("restart-required")});
|
|
_sceneryReloadListener.reset(new RequiredPropertyListener{kSceneryReloadRequiredProp, graphicsPropsXML->getChild("scenery-reload-required")});
|
|
_compositorReloadListener.reset(new RequiredPropertyListener{kCompositorReloadRequiredProp, graphicsPropsXML->getChild("compositor-reload-required")});
|
|
|
|
SGPropertyNode_ptr toSave = graphicsPropsXML->getChild("save-to-file");
|
|
if (toSave) {
|
|
for (const auto& p : toSave->getChildren("property")) {
|
|
string t = simgear::strutils::strip(p->getStringValue());
|
|
if (t.at(0) == '/') {
|
|
t = t.substr(1); // remove leading '/'
|
|
}
|
|
_propertiesToSave.push_back(t);
|
|
}
|
|
}
|
|
} catch (sg_exception& e) {
|
|
SG_LOG(SG_GUI, SG_ALERT, "Failed to read graphics-properties.xml");
|
|
}
|
|
|
|
globals->get_commands()->addCommand("apply-graphics-preset", do_apply_preset);
|
|
globals->get_commands()->addCommand("save-graphics-preset", do_save_preset);
|
|
globals->get_commands()->addCommand("list-graphics-presets", do_list_standard_presets);
|
|
}
|
|
|
|
void GraphicsPresets::update(double delta_time_sec)
|
|
{
|
|
SG_UNUSED(delta_time_sec);
|
|
}
|
|
|
|
void GraphicsPresets::shutdown()
|
|
{
|
|
globals->get_commands()->removeCommand("apply-graphics-preset");
|
|
globals->get_commands()->removeCommand("save-graphics-preset");
|
|
globals->get_commands()->removeCommand("list-graphics-presets");
|
|
|
|
_listener->unregisterFromProperties();
|
|
_listener.reset();
|
|
}
|
|
|
|
bool GraphicsPresets::applyCurrentPreset()
|
|
{
|
|
_listener->unregisterFromProperties();
|
|
|
|
const string presetId = fgGetString(kPresetPropPath);
|
|
GraphicsPresetInfo info;
|
|
const auto ok = loadStandardPreset(presetId, info);
|
|
if (!ok) {
|
|
return false;
|
|
}
|
|
|
|
addSentryBreadcrumb("loading graphics preset:" + presetId, "info");
|
|
|
|
return innerApplyPreset(info, true);
|
|
}
|
|
|
|
bool GraphicsPresets::loadStandardPreset(const std::string& id, GraphicsPresetInfo& info)
|
|
{
|
|
const auto path = globals->findDataPath("Video/" + id + "-preset.xml");
|
|
if (!path.exists()) {
|
|
SG_LOG(SG_GUI, SG_ALERT, "No such graphics preset '" << id << "' found");
|
|
return false;
|
|
}
|
|
|
|
return loadPresetXML(path, info);
|
|
}
|
|
|
|
|
|
auto GraphicsPresets::listPresets() -> GraphicsPresetVec
|
|
{
|
|
GraphicsPresetVec result;
|
|
|
|
auto videoPaths = globals->get_data_paths("Video");
|
|
for (const auto& vp : videoPaths) {
|
|
simgear::Dir videoDir(vp);
|
|
for (const auto& presetFile : videoDir.children(simgear::Dir::TYPE_FILE, "-preset.xml")) {
|
|
GraphicsPresetInfo info;
|
|
loadPresetXML(presetFile, info);
|
|
result.push_back(info);
|
|
}
|
|
} // of Video/ data dirs iteration
|
|
|
|
// Sort the resulting list by the order number or alphabetically if some
|
|
// presets have the same order number
|
|
sort(result.begin(), result.end(), [](auto &a, auto &b) {
|
|
if (a.orderNum != b.orderNum)
|
|
return a.orderNum < b.orderNum;
|
|
return a.name < b.name;
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
bool GraphicsPresets::applyCustomPreset(const SGPath& path)
|
|
{
|
|
GraphicsPresetInfo info;
|
|
const auto ok = loadPresetXML(path, info);
|
|
if (!ok) {
|
|
return false;
|
|
}
|
|
|
|
addSentryBreadcrumb("loading graphics preset from:" + path.utf8Str(), "info");
|
|
return innerApplyPreset(info, true);
|
|
}
|
|
|
|
bool GraphicsPresets::applyPresetByName(const std::string& name)
|
|
{
|
|
const auto presets = listPresets();
|
|
auto it = std::find_if(presets.begin(), presets.end(), [name](const GraphicsPresetInfo& pi) { return simgear::strutils::iequals(name, pi.name); });
|
|
if (it == presets.end()) {
|
|
SG_LOG(SG_GUI, SG_ALERT, "Couldn't find graphics preset with name:" << name);
|
|
return false;
|
|
}
|
|
|
|
fgSetString(kPresetPropPath, it->id);
|
|
return applyCurrentPreset();
|
|
}
|
|
|
|
bool GraphicsPresets::innerApplyPreset(const GraphicsPresetInfo& info, bool overwriteAutosaved)
|
|
{
|
|
fgSetString(kPresetNameProp, info.name);
|
|
fgSetString(kPresetDescriptionProp, info.description);
|
|
|
|
if (info.id == "custom") {
|
|
fgSetBool(kPresetActiveProp, false);
|
|
}
|
|
|
|
std::unordered_set<string> leafProps;
|
|
|
|
copyPropertiesIf(info.properties, globals->get_props(), [overwriteAutosaved, &leafProps](const SGPropertyNode* src) {
|
|
if (src->getParent() == nullptr)
|
|
return true; // root node passes
|
|
|
|
// due to the slightly odd way SGPropertyNode::getPath works, we
|
|
// don't need to omit settings here; it will be dropped
|
|
// automatically.
|
|
const auto path = src->getPath(true);
|
|
|
|
auto it = std::find_if(kWitelistedPrefixes.begin(), kWitelistedPrefixes.end(), [&path](const string& p) {
|
|
// if we're high up in the tree, eg looking at /sim, then we
|
|
// want to check if at least one prefix includes that path
|
|
if (path.length() < p.length()) {
|
|
return simgear::strutils::starts_with(p, path);
|
|
}
|
|
|
|
// if the prefix is longer (more specific) than our path, we
|
|
// want to consider the full prefix
|
|
return simgear::strutils::starts_with(path, p);
|
|
});
|
|
|
|
if (it == kWitelistedPrefixes.end()) {
|
|
return false; // skip entirely
|
|
}
|
|
|
|
// find the corresponding destination node
|
|
auto dstNode = globals->get_props()->getNode(path);
|
|
|
|
// if destination exists, and we're not over-writing, check its
|
|
// ARCHIVE flag.
|
|
if (!overwriteAutosaved && dstNode && (dstNode->getAttribute(SGPropertyNode::ARCHIVE) == false)) {
|
|
return false;
|
|
}
|
|
|
|
// only watch the leaf properties
|
|
const bool isLeaf = src->nChildren() == 0;
|
|
if (isLeaf) {
|
|
leafProps.insert(path);
|
|
}
|
|
return true; // easy, just copy it
|
|
});
|
|
|
|
_listener->unregisterFromProperties();
|
|
for (const auto& p : leafProps) {
|
|
_listener->registerWithProperty(fgGetNode(p));
|
|
}
|
|
|
|
fgSetBool(kPresetActiveProp, true);
|
|
return true;
|
|
}
|
|
|
|
void GraphicsPresets::clearPreset()
|
|
{
|
|
fgSetString(kPresetNameProp, "");
|
|
fgSetString(kPresetDescriptionProp, "");
|
|
fgSetBool(kPresetActiveProp, false);
|
|
_listener->unregisterFromProperties();
|
|
}
|
|
|
|
bool GraphicsPresets::loadPresetXML(const SGPath& p, GraphicsPresetInfo& info)
|
|
{
|
|
SGPropertyNode_ptr props(new SGPropertyNode);
|
|
|
|
try {
|
|
readProperties(p, props.get());
|
|
} catch (sg_exception& e) {
|
|
SG_LOG(SG_IO, SG_ALERT, "XML errors loading " << p.str() << "\n\t" << e.getFormattedMessage());
|
|
return false;
|
|
}
|
|
|
|
const string id = props->getStringValue("id");
|
|
const string rawName = props->getStringValue("name");
|
|
const string rawDesc = props->getStringValue("description");
|
|
int orderNum = props->getIntValue("order-num", 99);
|
|
|
|
if (id.empty() || rawName.empty() || rawDesc.empty()) {
|
|
SG_LOG(SG_IO, SG_ALERT, "Missing preset info loading: " << p.str());
|
|
return false;
|
|
}
|
|
|
|
info.id = id;
|
|
info.name = globals->get_locale()->getLocalizedString(rawName, "graphics-presets");
|
|
info.description = globals->get_locale()->getLocalizedString(rawDesc, "graphics-presets");
|
|
info.orderNum = orderNum;
|
|
|
|
if (info.name.empty())
|
|
info.name = rawName; // no translation defined
|
|
if (info.description.empty())
|
|
info.description = rawDesc;
|
|
|
|
info.properties = props->getChild("settings");
|
|
if (!info.properties) {
|
|
SG_LOG(SG_IO, SG_ALERT, "Missing settings loading: " << p.str());
|
|
return false;
|
|
}
|
|
|
|
// read devices list
|
|
info.devices.clear();
|
|
SGPropertyNode_ptr devices = props->getChild("devices");
|
|
if (devices) {
|
|
for (auto d : devices->getChildren("device")) {
|
|
const auto t = simgear::strutils::strip(d->getStringValue());
|
|
info.devices.push_back(t);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool GraphicsPresets::saveToXML(const SGPath& path, const std::string& name, const std::string& desc)
|
|
{
|
|
SGPropertyNode_ptr presetXML(new SGPropertyNode);
|
|
presetXML->setStringValue("id", path.file_base()); // without .xml
|
|
presetXML->setStringValue("name", name);
|
|
presetXML->setStringValue("description", desc);
|
|
|
|
auto settingsNode = presetXML->getChild("settings", 0, true);
|
|
|
|
for (const auto& path : _propertiesToSave) {
|
|
auto srcNode = fgGetNode(path);
|
|
if (!srcNode || !srcNode->hasValue())
|
|
continue;
|
|
|
|
auto dstNode = settingsNode->getNode(path, true);
|
|
copyProperties(srcNode, dstNode);
|
|
}
|
|
|
|
try {
|
|
sg_ofstream os(path, std::ios::out | std::ios::trunc);
|
|
writeProperties(os, presetXML, true /*write all*/);
|
|
} catch (sg_exception& e) {
|
|
SG_LOG(SG_GENERAL, SG_ALERT, "Failed to save presets file to:" << path << "\nt\tFailed:" << e.getFormattedMessage());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace flightgear
|