simgear/scene/model/SGReaderWriterXML.cxx: Added optional auto-generated tooltips for moving objects.

This allows one to see what properties are being used to control moving objects
such as needles and dials in the cockpit, or external moving objects such as
bomb doors.

The system works by optionally creating new animations with type=pick for all
animated objects. A side affect of this is that yellow highlighting of controls
from Ctrl-C will also highlight instrument needles in the cockpit and external
animations such as flaps, rudder, gear etc.

src/Model/acmodel.cxx:simgear::SGModelLib::loadModel() takes new param bool
autoTooltipsMaster and int autoTooltipsMasterMax, which is added into the
SGReaderWriterOptions that is passed to loadFile(). autoTooltipsMasterMax
limits the maximum number of tooltips, which appears to be necessary on 777 -
more than 45 can cause fg to fail to make progress.

The auto-generated tooltips show the name of the object and also the names and
values of the properties that control the animation. For example the tooltip
for a fuel gauge might say 'consumables/fuel/tank[1]/level-gal_imp=0.23456789'.
This commit is contained in:
Julian Smith 2019-08-30 19:06:49 +01:00
parent 9ccbf539c1
commit d9470c1407
4 changed files with 235 additions and 2 deletions

View File

@ -256,6 +256,208 @@ namespace {
} }
} }
struct DumpSGPropertyNode {
SGPropertyNode* _node;
const std::string& _indent;
DumpSGPropertyNode(SGPropertyNode* node, const std::string& indent="")
:
_node(node),
_indent(indent)
{}
friend std::ostream& operator << (std::ostream& out, const DumpSGPropertyNode& dump)
{
if (!dump._node) return out;
out << dump._indent << dump._node->getDisplayName() << "=" << dump._node->getStringValue() << "\n";
for (int i=0; i<dump._node->nChildren(); ++i) {
SGPropertyNode* child = dump._node->getChild(i);
out << DumpSGPropertyNode(child, dump._indent + " ");
}
return out;
}
};
struct DumpOsgNode {
osg::Node* _node;
const std::string& _indent;
DumpOsgNode(osg::Node* node, const std::string& indent="")
:
_node(node),
_indent(indent)
{}
friend std::ostream& operator << (std::ostream& out, const DumpOsgNode& dump)
{
if (!dump._node) return out;
out << dump._indent << dump._node->getName() << "\n";
osg::Group* group = dynamic_cast<osg::Group*>(dump._node);
if (group) {
for (unsigned i=0; i<group->getNumChildren(); ++i) {
out << DumpOsgNode(group->getChild(i), dump._indent + " ");
}
}
return out;
}
};
/* Finds all names recursively in an osg::Node. */
struct OSGNodeGetNames
{
std::vector<std::string> names;
OSGNodeGetNames(osg::Node* node=NULL)
{
add(node);
}
void add(osg::Node* node)
{
if (!node) return;
const std::string& name = node->getName();
if (name != "") names.push_back(name);
osg::Group* group = dynamic_cast<osg::Group*>(node);
if (group) {
for (unsigned i=0; i<group->getNumChildren(); ++i) {
add(group->getChild(i));
}
}
}
};
/* For each existing animation in <props>, we add a tooltip showing information
on the properties that the animation depends on.
At runtime, the tooltips only show if sim/animation-tooltips is true.
The dummy animations will show up as yellow (like clickable items) if the user
presses Ctrl-C, even if tooltips aren't showing because sim/animation-tooltips
is false. */
void addTooltipAnimations(const SGPath& path, SGPropertyNode_ptr props, osg::ref_ptr<osg::Node> model, int autoTooltipsMasterMax)
{
OSGNodeGetNames model_names;
if (0) {
/* Include all names in the .asi file, in an attempt to make
tooltips activate for more than just the needle in instrument dials
for example. This doesn't seem to help. */
model_names.add(model);
if (!model_names.names.empty()) {
/* First name will be filename of .ac file. */
model_names.names.erase(model_names.names.begin());
}
}
/* For each animation add an extra animation with type=pick containing
set-tooltip. We use the object-name as the tooltip-id, and we use the
animation's property name/value(s) in the tooltip label. */
PropertyList animations = props->getChildren("animation");
SG_LOG(SG_INPUT, SG_DEBUG, "animations.size()=" << animations.size()
<< " path=" << path
<< " props=" << (props ? props->getPath() : "")
);
for (unsigned i = 0; i < animations.size(); ++i) {
SGPropertyNode* animation = animations[i];
if (!strcmp(animation->getStringValue("type"), "pick")) {
/* There appear to be many of these, and we end up consuming
GB's of memory if we install a tooltip for each one, so ignore.
*/
continue;
}
PropertyList properties = animation->getChildren("property");
if (properties.empty()) {
/* We can't really show anything useful in tooltip. */
continue;
}
PropertyList objectnames(animation->getChildren("object-name"));
if (objectnames.empty() && model_names.names.empty()) {
continue;
}
/* Make a unique tooltip-id. */
std::string id = "auto-tooltip-";
{
static int id_n = 0;
id_n += 1;
if (autoTooltipsMasterMax > 0 && id_n > autoTooltipsMasterMax) {
continue;
}
std::ostringstream s;
s << id_n;
id += s.str();
}
/* If we reach here, we create a new dummy animation with type="pick"
that will implement a tooltip with information about <animation>. */
SGPropertyNode* new_animation = props->addChild("animation");
new_animation->addChild("type")->setValue("pick");
/* Use <animation>'s object-names so that our tooltip appears whenever
the user hovers over <animation>'s objects. */
for (unsigned i=0; i<objectnames.size(); ++i) {
std::string objectname = objectnames[i]->getStringValue();
new_animation->addChild("object-name")->setStringValue(objectname);
}
for (auto name: model_names.names) {
bool found = false;
for (auto objectname: objectnames) {
if (name == objectname->getStringValue()) {
found = true;
break;
}
}
if (!found) {
new_animation->addChild("object-name")->setStringValue(name);
}
}
SGPropertyNode* hovered_binding = new_animation->addChild("hovered")->addChild("binding");
hovered_binding->addChild("command")->setValue("set-tooltip");
hovered_binding->addChild("condition")->addChild("property")->setValue("sim/animation-tooltips");
hovered_binding->addChild("tooltip-id")->setStringValue(id);
/* Build up printf-style label string showing the property values
that <animation> depend on. */
std::string label;
for (unsigned i=0; i<properties.size(); ++i) {
if (i > 0) label += " ";
SGPropertyNode* p = properties[i];
/* Using an alias here (rather then just copying the path) ensures
things work if <p> is itself an alias. */
hovered_binding->addChild("property")->alias(p);
if (p->isAlias()) {
/* We only get things like "/rpm[0]" here, rather than the full
path of the property to which p points. */
label += p->getAliasTarget()->getPath();
}
else {
label += p->getStringValue();
}
label += "=%s";
}
hovered_binding->addChild("label")->setStringValue(label);
SG_LOG(SG_INPUT, SG_BULK, "have added new_animation:\n"
<< DumpSGPropertyNode(new_animation, " ")
);
}
SG_LOG(SG_INPUT, SG_BULK, "props for path=" << path << ":\n"
<< DumpSGPropertyNode(props, " ")
);
}
static std::tuple<int, osg::Node *> static std::tuple<int, osg::Node *>
sgLoad3DModel_internal(const SGPath& path, sgLoad3DModel_internal(const SGPath& path,
const osgDB::Options* dbOptions, const osgDB::Options* dbOptions,
@ -295,6 +497,10 @@ sgLoad3DModel_internal(const SGPath& path,
<< t.getFormattedMessage()); << t.getFormattedMessage());
throw; throw;
} }
if (options->getAutoTooltipsMaster()) {
addTooltipAnimations(path, props, model, options->getAutoTooltipsMasterMax());
}
if (overlay) if (overlay)
copyProperties(overlay, props); copyProperties(overlay, props);

View File

@ -128,7 +128,9 @@ osg::Node*
SGModelLib::loadModel(const string &path, SGModelLib::loadModel(const string &path,
SGPropertyNode *prop_root, SGPropertyNode *prop_root,
SGModelData *data, SGModelData *data,
bool load2DPanels) bool load2DPanels,
bool autoTooltipsMaster,
int autoTooltipsMasterMax)
{ {
osg::ref_ptr<SGReaderWriterOptions> opt; osg::ref_ptr<SGReaderWriterOptions> opt;
opt = SGReaderWriterOptions::copyOrCreate(osgDB::Registry::instance()->getOptions()); opt = SGReaderWriterOptions::copyOrCreate(osgDB::Registry::instance()->getOptions());
@ -140,6 +142,9 @@ SGModelLib::loadModel(const string &path,
opt->setLoadPanel(static_panelFunc); opt->setLoadPanel(static_panelFunc);
} }
opt->setAutoTooltipsMaster(autoTooltipsMaster);
opt->setAutoTooltipsMasterMax(autoTooltipsMasterMax);
osg::Node *n = loadFile(path, opt.get()); osg::Node *n = loadFile(path, opt.get());
if (n && n->getName().empty()) if (n && n->getName().empty())
n->setName("Direct loaded model \"" + path + "\""); n->setName("Direct loaded model \"" + path + "\"");

View File

@ -59,7 +59,9 @@ public:
// data->modelLoaded() will be called after the model is loaded // data->modelLoaded() will be called after the model is loaded
static osg::Node* loadModel(const std::string &path, static osg::Node* loadModel(const std::string &path,
SGPropertyNode *prop_root = NULL, SGPropertyNode *prop_root = NULL,
SGModelData *data=0, bool load2DPanels=false); SGModelData *data=0, bool load2DPanels=false,
bool autoTooltipsMaster=false,
int autoTooltipsMasterMax=0);
// Load a 3D model (any format) through the DatabasePager. // Load a 3D model (any format) through the DatabasePager.
// This function initially just returns a proxy node that refers to // This function initially just returns a proxy node that refers to

View File

@ -55,6 +55,8 @@ public:
_load_panel(0), _load_panel(0),
_instantiateEffects(false), _instantiateEffects(false),
_instantiateMaterialEffects(false), _instantiateMaterialEffects(false),
_autoTooltipsMaster(false),
_autoTooltipsMasterMax(0),
_LoadOriginHint(ORIGIN_MODEL) _LoadOriginHint(ORIGIN_MODEL)
{ } { }
SGReaderWriterOptions(const std::string& str) : SGReaderWriterOptions(const std::string& str) :
@ -63,6 +65,8 @@ public:
_load_panel(0), _load_panel(0),
_instantiateEffects(false), _instantiateEffects(false),
_instantiateMaterialEffects(false), _instantiateMaterialEffects(false),
_autoTooltipsMaster(false),
_autoTooltipsMasterMax(0),
_LoadOriginHint(ORIGIN_MODEL) _LoadOriginHint(ORIGIN_MODEL)
{ } { }
SGReaderWriterOptions(const osgDB::Options& options, SGReaderWriterOptions(const osgDB::Options& options,
@ -72,6 +76,8 @@ public:
_load_panel(0), _load_panel(0),
_instantiateEffects(false), _instantiateEffects(false),
_instantiateMaterialEffects(false), _instantiateMaterialEffects(false),
_autoTooltipsMaster(false),
_autoTooltipsMasterMax(0),
_LoadOriginHint(ORIGIN_MODEL) _LoadOriginHint(ORIGIN_MODEL)
{ } { }
SGReaderWriterOptions(const SGReaderWriterOptions& options, SGReaderWriterOptions(const SGReaderWriterOptions& options,
@ -88,6 +94,8 @@ public:
_instantiateMaterialEffects(options._instantiateMaterialEffects), _instantiateMaterialEffects(options._instantiateMaterialEffects),
_materialName(options._materialName), _materialName(options._materialName),
_sceneryPathSuffixes(options._sceneryPathSuffixes), _sceneryPathSuffixes(options._sceneryPathSuffixes),
_autoTooltipsMaster(options._autoTooltipsMaster),
_autoTooltipsMasterMax(options._autoTooltipsMasterMax),
_LoadOriginHint(ORIGIN_MODEL) _LoadOriginHint(ORIGIN_MODEL)
{ } { }
@ -142,6 +150,16 @@ public:
void setSceneryPathSuffixes(const string_list& suffixes) void setSceneryPathSuffixes(const string_list& suffixes)
{ _sceneryPathSuffixes = suffixes; } { _sceneryPathSuffixes = suffixes; }
bool getAutoTooltipsMaster() const
{ return _autoTooltipsMaster; }
void setAutoTooltipsMaster(bool autoTooltipsMaster)
{ _autoTooltipsMaster = autoTooltipsMaster; }
int getAutoTooltipsMasterMax() const
{ return _autoTooltipsMasterMax; }
void setAutoTooltipsMasterMax(int autoTooltipsMasterMax)
{ _autoTooltipsMasterMax = autoTooltipsMasterMax; }
static SGReaderWriterOptions* copyOrCreate(const osgDB::Options* options); static SGReaderWriterOptions* copyOrCreate(const osgDB::Options* options);
static SGReaderWriterOptions* fromPath(const SGPath& path); static SGReaderWriterOptions* fromPath(const SGPath& path);
@ -176,6 +194,8 @@ private:
bool _instantiateMaterialEffects; bool _instantiateMaterialEffects;
string _materialName; string _materialName;
string_list _sceneryPathSuffixes; string_list _sceneryPathSuffixes;
bool _autoTooltipsMaster;
int _autoTooltipsMasterMax;
SGGeod _geod; SGGeod _geod;
mutable LoadOriginHint _LoadOriginHint; mutable LoadOriginHint _LoadOriginHint;
}; };