From 1bafe15c4cc5a05509ee06b50cfe123f6f36ef54 Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Tue, 5 Oct 2021 22:25:08 +0100 Subject: [PATCH] Added highlighting system. If /sim/highlighting/enabled is true, we highlight animated objects under the pointer, and also highlight other objects that are animated by the same or related properties. The intent here is to be able to give a visual indication of what cockpit controls do or what cockpit controls affect particular aircraft objects - for example moving the pointer over the flaps will highlight the flaps and also highlight any controls or rotary indicators in the cockpit that are associated with the flaps. To make this work, we have to discover associations between properties. This is currently done for YASim (e.g. associations between /controls/flight/flaps and /surface-positions/flap-pos-norm) and autopilot filters (e.g. with the 777, digital filters associate /controls/flight/rudder-nul with /fcs/fbw/yaw/rudder-ratio-out). We don't currently gather associations between properties in JSBSim We also detect associations between dialogs, menus and keypresses and properties, which is used to populate /sim/highlighting/current with information about dialogs, menus and keypresses that are associated with the currently highlighted nodes' properties. Details: src/GUI/Highlight.cxx src/GUI/Highlight.hxx src/GUI/CMakeLists.txt src/Main/fg_init.cxx New subsystem called 'highlight', with support for registering and recovering links between menus, dialogs, keypresses and OSG node animations. Provides a function Highlight::highlight_nodes() which highlights related nodes using internal NodeHighlighting class, and populates /sim/highlighting/current with information about related dialogs, menus and keypresses. The NodeHighlighting class works by making nodes use an alternative StateSet which shows up as a distinct material on screen. We remember each highlighted node's original StateSet so that we can un-highlight. We update the material parameters using a listener for /sim/highlighting/material, which allows some control over the appearence of highlighted nodes. src/FDM/flight.cxx src/FDM/flight.hxx Added virtual method FGInterface::property_associations() which returns property associations from the FDM. Default implementation returns empty set. Implemented in YASim, but not (yet) in JSBSim. Uses a simple function pointer at the moment to avoid requring FDMs to use recent C++ features. src/FDM/YASim/FGFDM.cpp src/FDM/YASim/FGFDM.hpp src/FDM/YASim/YASim.cxx src/FDM/YASim/YASim.hxx Gathers information about property associations on startup such as /controls/flight/flaps => /surface-positions/flap-pos-norm, then YASim::property_associations() overrides default implementation to return these associations. src/Autopilot/analogcomponent.cxx src/Autopilot/analogcomponent.hxx src/Autopilot/digitalfilter.cxx src/Autopilot/inputvalue.cxx src/Autopilot/inputvalue.hxx Filters now gather information about their input/output properties and register with Highlight::add_property_property(). For example this makes highlighting work on the 777, where pilot controls affect control surfaces only via filters. src/GUI/new_gui.cxx Scan menus, keypresses and dialogs and register associations with Highlight::add_*(). src/GUI/property_list.cxx src/GUI/property_list.hxx src/GUI/FGPUIDialog.cxx Added flag to property-list. If set, we don't show .. or . items and don't respond to mouse/keyboard. Used by fgdata's new Highlighting dialogue. src/Model/acmodel.cxx src/Model/acmodel.hxx Visit the user aircraft's scene graph when it is loaded, gathering information about osg::Node's that are animated by properties, and register these associations with Highlight::add_property_node(). src/Viewer/renderer.cxx When scanning for pick highlights, use Highlight::highlight_nodes() to highlight animated objects under the pointer and related objects. --- src/Autopilot/analogcomponent.cxx | 8 + src/Autopilot/analogcomponent.hxx | 6 + src/Autopilot/digitalfilter.cxx | 93 ++++- src/Autopilot/inputvalue.cxx | 16 + src/Autopilot/inputvalue.hxx | 3 + src/FDM/YASim/FGFDM.cpp | 71 +++- src/FDM/YASim/FGFDM.hpp | 20 + src/FDM/YASim/YASim.cxx | 8 + src/FDM/YASim/YASim.hxx | 5 + src/FDM/flight.cxx | 8 + src/FDM/flight.hxx | 8 + src/GUI/CMakeLists.txt | 2 + src/GUI/FGPUIDialog.cxx | 5 +- src/GUI/Highlight.cxx | 666 ++++++++++++++++++++++++++++++ src/GUI/Highlight.hxx | 114 +++++ src/GUI/new_gui.cxx | 93 ++++- src/GUI/property_list.cxx | 10 +- src/GUI/property_list.hxx | 5 +- src/Main/fg_init.cxx | 2 + src/Model/acmodel.cxx | 80 ++++ src/Model/acmodel.hxx | 1 + src/Viewer/renderer.cxx | 11 + 22 files changed, 1204 insertions(+), 31 deletions(-) create mode 100644 src/GUI/Highlight.cxx create mode 100644 src/GUI/Highlight.hxx diff --git a/src/Autopilot/analogcomponent.cxx b/src/Autopilot/analogcomponent.cxx index f161d88cf..549c69bc0 100644 --- a/src/Autopilot/analogcomponent.cxx +++ b/src/Autopilot/analogcomponent.cxx @@ -120,3 +120,11 @@ bool AnalogComponent::configure( SGPropertyNode& cfg_node, return Component::configure(cfg_node, cfg_name, prop_root); } + +void AnalogComponent::collectDependentProperties(std::set& props) const +{ + _valueInput.collectDependentProperties(props); + _referenceInput.collectDependentProperties(props); + _minInput.collectDependentProperties(props); + _maxInput.collectDependentProperties(props); +} diff --git a/src/Autopilot/analogcomponent.hxx b/src/Autopilot/analogcomponent.hxx index f7e6b76fa..f5682baeb 100644 --- a/src/Autopilot/analogcomponent.hxx +++ b/src/Autopilot/analogcomponent.hxx @@ -139,6 +139,12 @@ protected: public: const PeriodicalValue * getPeriodicalValue() const { return _periodical; } + + /** + Add to all properties that are used by this component. Similar to + SGExpression::collectDependentProperties(). + */ + void collectDependentProperties(std::set& props) const; }; inline void AnalogComponent::disabled( double dt ) diff --git a/src/Autopilot/digitalfilter.cxx b/src/Autopilot/digitalfilter.cxx index 56f1e8a9e..f4292c232 100644 --- a/src/Autopilot/digitalfilter.cxx +++ b/src/Autopilot/digitalfilter.cxx @@ -26,6 +26,8 @@ // #include "digitalfilter.hxx" +#include +#include
#include #include @@ -52,7 +54,7 @@ class DigitalFilterImplementation: SGPropertyNode& prop_root ) = 0; void setDigitalFilter( DigitalFilter * digitalFilter ) { _digitalFilter = digitalFilter; } - + virtual void collectDependentProperties(std::set& props) const = 0; protected: DigitalFilter * _digitalFilter; }; @@ -68,6 +70,10 @@ protected: public: GainFilterImplementation() : _gainInput(1.0) {} double compute( double dt, double input ); + virtual void collectDependentProperties(std::set& props) const + { + _gainInput.collectDependentProperties(props); + } }; class ReciprocalFilterImplementation : public GainFilterImplementation { @@ -85,6 +91,11 @@ public: DerivativeFilterImplementation(); double compute( double dt, double input ); virtual void initialize( double initvalue ); + virtual void collectDependentProperties(std::set& props) const + { + GainFilterImplementation::collectDependentProperties(props); + _TfInput.collectDependentProperties(props); + } }; class ExponentialFilterImplementation : public GainFilterImplementation { @@ -99,6 +110,11 @@ public: ExponentialFilterImplementation(); double compute( double dt, double input ); virtual void initialize( double initvalue ); + virtual void collectDependentProperties(std::set& props) const + { + GainFilterImplementation::collectDependentProperties(props); + _TfInput.collectDependentProperties(props); + } }; class MovingAverageFilterImplementation : public DigitalFilterImplementation { @@ -113,6 +129,10 @@ public: MovingAverageFilterImplementation(); double compute( double dt, double input ); virtual void initialize( double initvalue ); + virtual void collectDependentProperties(std::set& props) const + { + _samplesInput.collectDependentProperties(props); + } }; class NoiseSpikeFilterImplementation : public DigitalFilterImplementation { @@ -126,6 +146,10 @@ public: NoiseSpikeFilterImplementation(); double compute( double dt, double input ); virtual void initialize( double initvalue ); + virtual void collectDependentProperties(std::set& props) const + { + _rateOfChangeInput.collectDependentProperties(props); + } }; class RateLimitFilterImplementation : public DigitalFilterImplementation { @@ -140,6 +164,11 @@ public: RateLimitFilterImplementation(); double compute( double dt, double input ); virtual void initialize( double initvalue ); + virtual void collectDependentProperties(std::set& props) const + { + _rateOfChangeMax.collectDependentProperties(props); + _rateOfChangeMin.collectDependentProperties(props); + } }; class IntegratorFilterImplementation : public GainFilterImplementation { @@ -156,6 +185,13 @@ public: IntegratorFilterImplementation(); double compute( double dt, double input ); virtual void initialize( double initvalue ); + virtual void collectDependentProperties(std::set& props) const + { + GainFilterImplementation::collectDependentProperties(props); + _TfInput.collectDependentProperties(props); + _minInput.collectDependentProperties(props); + _maxInput.collectDependentProperties(props); + } }; // integrates x" + ax' + bx + c = 0 @@ -174,6 +210,13 @@ public: DampedOscillationFilterImplementation(); double compute( double dt, double input ); virtual void initialize( double initvalue ); + virtual void collectDependentProperties(std::set& props) const + { + GainFilterImplementation::collectDependentProperties(props); + _aInput.collectDependentProperties(props); + _bInput.collectDependentProperties(props); + _cInput.collectDependentProperties(props); + } }; class HighPassFilterImplementation : public GainFilterImplementation { @@ -188,6 +231,11 @@ public: HighPassFilterImplementation(); double compute( double dt, double input ); virtual void initialize( double initvalue ); + virtual void collectDependentProperties(std::set& props) const + { + GainFilterImplementation::collectDependentProperties(props); + _TfInput.collectDependentProperties(props); + } }; class LeadLagFilterImplementation : public GainFilterImplementation { protected: @@ -202,6 +250,12 @@ public: LeadLagFilterImplementation(); double compute( double dt, double input ); virtual void initialize( double initvalue ); + virtual void collectDependentProperties(std::set& props) const + { + GainFilterImplementation::collectDependentProperties(props); + _TfaInput.collectDependentProperties(props); + _TfbInput.collectDependentProperties(props); + } }; class CoherentNoiseFilterImplementation : public DigitalFilterImplementation @@ -221,6 +275,10 @@ public: CoherentNoiseFilterImplementation(); double compute(double dt, double input) override; void initialize(double initvalue) override; + virtual void collectDependentProperties(std::set& props) const + { + _amplitude.collectDependentProperties(props); + } }; /* --------------------------------------------------------------------------------- */ @@ -846,20 +904,37 @@ bool DigitalFilter::configure( SGPropertyNode& prop_root, { SGPropertyNode_ptr child = cfg.getChild(i); std::string cname(child->getName()); - - if( !_implementation->configure(*child, cname, prop_root) - && !configure(*child, cname, prop_root) - && cname != "type" - && cname != "params" ) // 'params' is usually used to specify parameters - // in PropertList files. + bool ok = false; + if (!ok) ok = _implementation->configure(*child, cname, prop_root); + if (!ok) ok = configure(*child, cname, prop_root); + if (!ok) ok = (cname == "type"); + if (!ok) ok = (cname == "params"); // 'params' is usually used to specify parameters in PropertList files. + if (!ok) { SG_LOG ( SG_AUTOPILOT, - SG_WARN, + SG_ALERT, "DigitalFilter: unknown config node: " << cname ); + } } - + + /* Send information about associations between our input and output + properties to Highlight. */ + std::set inputs; + _implementation->collectDependentProperties(inputs); + collectDependentProperties(inputs); + + Highlight* highlight = globals->get_subsystem(); + for (auto in: inputs) { + for (auto& out: _output_list) { + highlight->add_property_property( + in->getPath(true /*simplify*/), + out->getPath(true /*simplify*/) + ); + } + } + return true; } diff --git a/src/Autopilot/inputvalue.cxx b/src/Autopilot/inputvalue.cxx index 501874436..1e597edf6 100644 --- a/src/Autopilot/inputvalue.cxx +++ b/src/Autopilot/inputvalue.cxx @@ -225,3 +225,19 @@ double InputValue::get_value() const return _abs ? fabs(value) : value; } +void InputValue::collectDependentProperties(std::set& props) const +{ + if (_property) props.insert(_property); + if (_offset) _offset->collectDependentProperties(props); + if (_scale) _scale->collectDependentProperties(props); + if (_min) _min->collectDependentProperties(props); + if (_max) _max->collectDependentProperties(props); + if (_expression) _expression->collectDependentProperties(props); +} + +void InputValueList::collectDependentProperties(std::set& props) const +{ + for (auto& iv: *this) { + iv->collectDependentProperties(props); + } +} diff --git a/src/Autopilot/inputvalue.hxx b/src/Autopilot/inputvalue.hxx index de5dd4900..aba7676d9 100644 --- a/src/Autopilot/inputvalue.hxx +++ b/src/Autopilot/inputvalue.hxx @@ -109,6 +109,7 @@ public: return _condition == NULL ? true : _condition->test(); } + void collectDependentProperties(std::set& props) const; }; /** @@ -134,6 +135,8 @@ class InputValueList : public std::vector { InputValue_ptr input = get_active(); return input == NULL ? _def : input->get_value(); } + + void collectDependentProperties(std::set& props) const; private: double _def; diff --git a/src/FDM/YASim/FGFDM.cpp b/src/FDM/YASim/FGFDM.cpp index 06e1d7c68..244c0a15e 100644 --- a/src/FDM/YASim/FGFDM.cpp +++ b/src/FDM/YASim/FGFDM.cpp @@ -62,6 +62,18 @@ FGFDM::~FGFDM() delete _turb; } +void FGFDM::property_associations( + void* ref, + void (*fn)(void* ref, const std::string& from, const std::string& to) + ) +{ + for (auto& a: _property_to_properties) { + for (auto& b: a.second) { + fn(ref, a.first, b); + } + } +} + void FGFDM::iterate(float dt) { getExternalInput(dt); @@ -209,12 +221,45 @@ void FGFDM::init() _airplane.getModel()->setTurbulence(_turb); } +void FGFDM::endElement(const char* name) +{ + _xml_depth -= 1; +} + // Not the worlds safest parser. But it's short & sweet. void FGFDM::startElement(const char* name, const XMLAttributes &a) { + _xml_depth += 1; //XMLAttributes* a = (XMLAttributes*)&atts; float v[3] {0,0,0}; + if (_xml_depth < _xml_last_control_depth) { + // We use _control_to_input_properties and + // _control_to_output_properties to link input/output properties via + // their 'control' attributes. For example a vstab might have: + // + // + // + // + // When parsing we associate FLAP0 with + // /controls/flight/flaps in _control_to_input_properties. Similarly + // when parsing we associate FLAPS0 with + // /surface-positions/flap-pos-norm in _control_to_output_properties. + // + // We can then use _control_to_input_properties and + // _control_to_output_properties to associate properties + // that are associated with the same control - in this case + // /controls/flight/flaps and /surface-positions/flap-pos-norm are both + // associated with FLAPS0. + // + // To make this work, we need to clear these maps when we have finished + // parsing each container of and , + // which we do here. + // + _control_to_input_properties.clear(); + _control_to_output_properties.clear(); + } + if(!strcmp(name, "airplane")) { parseAirplane(&a); } else if(!strcmp(name, "approach") || !strcmp(name, "cruise")) { parseApproachCruise(&a, name); @@ -255,8 +300,14 @@ void FGFDM::startElement(const char* name, const XMLAttributes &a) ((Thruster*)_currObj)->setDirection(v); } else if(!strcmp(name, "control-setting")) { parseControlSetting(&a); } - else if(!strcmp(name, "control-input")) { parseControlIn(&a); } - else if(!strcmp(name, "control-output")) { parseControlOut(&a); } + else if(!strcmp(name, "control-input")) { + _xml_last_control_depth = _xml_depth; + parseControlIn(&a); + } + else if(!strcmp(name, "control-output")) { + _xml_last_control_depth = _xml_depth; + parseControlOut(&a); + } else if(!strcmp(name, "control-speed")) { parseControlSpeed(&a); } else { SG_LOG(SG_FLIGHT,SG_ALERT,"Unexpected tag '" << name << "' found in YASim aircraft description"); @@ -1201,6 +1252,14 @@ void FGFDM::parseControlIn(const XMLAttributes* a) dst1 = attrf(a, "dst1"); } _airplane.addControlInput(a->getValue("axis"), control, _currObj, _wingSection, opt, src0, src1, dst0, dst1); + + // Detect new associations between properties. + std::string control_name = a->getValue("control"); + std::string in = a->getValue("axis"); + _control_to_input_properties[control_name].insert(in); + for (auto& out: _control_to_output_properties[control_name]) { + _property_to_properties[in].insert(out); + } } void FGFDM::parseControlOut(const XMLAttributes* a) @@ -1220,6 +1279,14 @@ void FGFDM::parseControlOut(const XMLAttributes* a) p->min = attrf(a, "min", cm->rangeMin(control)); p->max = attrf(a, "max", cm->rangeMax(control)); _controlOutputs.add(p); + + // Detect new associations between properties. + std::string control_name = a->getValue("control"); + std::string out = a->getValue("prop"); + _control_to_output_properties[control_name].insert(out); + for (auto& in: _control_to_input_properties[control_name]) { + _property_to_properties[in].insert(out); + } } void FGFDM::parseControlSpeed(const XMLAttributes* a) diff --git a/src/FDM/YASim/FGFDM.hpp b/src/FDM/YASim/FGFDM.hpp index 30663721a..c9b03f9dd 100644 --- a/src/FDM/YASim/FGFDM.hpp +++ b/src/FDM/YASim/FGFDM.hpp @@ -7,6 +7,11 @@ #include "Airplane.hpp" #include "Vector.hpp" +#include +#include +#include + + namespace yasim { class Wing; @@ -27,8 +32,14 @@ public: // XML parsing callback from XMLVisitor virtual void startElement(const char* name, const XMLAttributes &atts); + virtual void endElement(const char* name); float getVehicleRadius(void) const { return _vehicle_radius; } + + void property_associations( + void* ref, + void (*fn)(void* ref, const std::string& from, const std::string& to) + ); private: struct EngRec { @@ -118,6 +129,8 @@ private: Airplane::Configuration _airplaneCfg; int _nextEngine {0}; int _wingSection {0}; + int _xml_depth {0}; + int _xml_last_control_depth {0}; class FuelProps { @@ -159,6 +172,13 @@ private: SGPropertyNode_ptr _aryN; SGPropertyNode_ptr _arzN; SGPropertyNode_ptr _cg_xmacN; + + // Used on startup when gathering information about associated properties, + // e.g. /controls/flight/flaps and /surface-positions/flap-pos-norm. + std::map> _control_to_input_properties; + std::map> _control_to_output_properties; + std::map> _property_to_properties; + }; }; // namespace yasim diff --git a/src/FDM/YASim/YASim.cxx b/src/FDM/YASim/YASim.cxx index 1647ddca7..bf8f54169 100644 --- a/src/FDM/YASim/YASim.cxx +++ b/src/FDM/YASim/YASim.cxx @@ -54,6 +54,14 @@ YASim::~YASim() _gearProps.clear(); } +void YASim::property_associations( + void* ref, + void (*fn)(void* ref, const std::string& from, const std::string& to) + ) +{ + return _fdm->property_associations(ref, fn); +} + void YASim::report() { Airplane* a = _fdm->getAirplane(); diff --git a/src/FDM/YASim/YASim.hxx b/src/FDM/YASim/YASim.hxx index 37bfa6c4d..f51979db0 100644 --- a/src/FDM/YASim/YASim.hxx +++ b/src/FDM/YASim/YASim.hxx @@ -21,6 +21,11 @@ public: // Subsystem identification. static const char* staticSubsystemClassId() { return "yasim"; } + void property_associations( + void* ref, + void (*fn)(void* ref, const std::string& from, const std::string& to) + ) override; + private: void report(); void copyFromYASim(); diff --git a/src/FDM/flight.cxx b/src/FDM/flight.cxx index 1909c648c..3008dcf8f 100644 --- a/src/FDM/flight.cxx +++ b/src/FDM/flight.cxx @@ -233,6 +233,14 @@ FGInterface::common_init () SG_LOG( SG_FLIGHT, SG_INFO, "End common FDM init" ); } +void FGInterface::property_associations( + void* ref, + void (*fn)(void* ref, const std::string& from, const std::string& to) + ) +{ + /* Do nothing by default. */ +} + /** * Bind getters and setters to properties. diff --git a/src/FDM/flight.hxx b/src/FDM/flight.hxx index 83fb11f75..a807f28bb 100644 --- a/src/FDM/flight.hxx +++ b/src/FDM/flight.hxx @@ -440,6 +440,14 @@ public: //perform initializion that is common to all FDM's void common_init(); + // Makes possibly multiple calls of fn(ref, ...) with pairs of property + // paths that are associated in the FDM. Default implementation does + // nothing. Used by the Highlight subsystem. + virtual void property_associations( + void* ref, + void (*fn)(void* ref, const std::string& from, const std::string& to) + ); + // Positions virtual void set_Latitude(double lat); // geocentric virtual void set_Longitude(double lon); diff --git a/src/GUI/CMakeLists.txt b/src/GUI/CMakeLists.txt index 21289ecf7..32b15184c 100644 --- a/src/GUI/CMakeLists.txt +++ b/src/GUI/CMakeLists.txt @@ -22,6 +22,7 @@ set(SOURCES PUIFileDialog.cxx MouseCursor.cxx MessageBox.cxx + Highlight.cxx ) set(HEADERS @@ -43,6 +44,7 @@ set(HEADERS PUIFileDialog.hxx MouseCursor.hxx MessageBox.hxx + Highlight.hxx ) if(WIN32) diff --git a/src/GUI/FGPUIDialog.cxx b/src/GUI/FGPUIDialog.cxx index fe880da07..8b6eee2d3 100644 --- a/src/GUI/FGPUIDialog.cxx +++ b/src/GUI/FGPUIDialog.cxx @@ -944,7 +944,10 @@ FGPUIDialog::makeObject(SGPropertyNode* props, int parentWidth, int parentHeight return obj; } else if (type == "property-list") { - PropertyList* obj = new PropertyList(x, y, x + width, y + height, globals->get_props()); + PropertyList* obj = new PropertyList(x, y, x + width, y + height, + globals->get_props(), + props->getBoolValue("readonly", false) + ); if (presetSize) obj->setSize(width, height); setupObject(obj, props); diff --git a/src/GUI/Highlight.cxx b/src/GUI/Highlight.cxx new file mode 100644 index 000000000..9fc1bd658 --- /dev/null +++ b/src/GUI/Highlight.cxx @@ -0,0 +1,666 @@ +#include "Highlight.hxx" +#include "simgear/misc/strutils.hxx" + +#include +#include
+#include +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include + + +/* Returns translated string for specified menu/menuitem name. */ +static std::string s_get_menu_name(SGPropertyNode* node) +{ + /* We use 'label' if available, otherwise we translate 'name' using + sim/intl/locale/... */ + std::string name = node->getStringValue("label", ""); + if (name != "") return name; + name = node->getStringValue("name"); + return globals->get_props()->getStringValue("sim/intl/locale/strings/menu/" + name, name.c_str()); +} + +std::string HighlightMenu::description() const +{ + std::string ret = std::to_string(this->menu) + "/" + std::to_string(this->item) + ": "; + SGPropertyNode* menubar = globals->get_props()->getNode("sim/menubar/default"); + if (menubar) + { + SGPropertyNode* menu = menubar->getChild("menu", this->menu); + if (menu) + { + SGPropertyNode* item = menu->getChild("item", this->item); + if (item) + { + ret += s_get_menu_name(menu) + '/' + s_get_menu_name(item); + } + } + } + return ret; +} + +static bool operator< (const HighlightMenu& a, const HighlightMenu& b) +{ + if (a.menu != b.menu) return a.menu < b.menu; + return a.item < b.item; +} + +struct NameValue +{ + NameValue(const std::string& name, const std::string& value) + : name(name), value(value) + {} + std::string name; + std::string value; + bool operator< (const NameValue& rhs) const + { + if (name != rhs.name) return name < rhs.name; + return value < rhs.value; + } +}; + +static std::map s_property_to_info; + +static std::map, std::set> s_node_to_properties; +static std::map> s_dialog_to_properties; +static std::map> s_keypress_to_properties; +static std::map> s_menu_to_properties; + +static std::map s_menu_to_dialog; +static std::map> s_dialog_to_menus; + +static std::map> s_property_to_properties; +static std::map> s_property_from_properties; + +static std::set s_property_tree_items; + +static SGPropertyNode_ptr s_prop_enabled = nullptr; +static bool s_output_stats = false; + +static std::unique_ptr s_node_highlighting; + +std::unique_ptr s_fdm_initialised_listener; + + +/* Handles highlighting of individual nodes by changing their StateSet to +our internal StateSet. Also changes our StateSet's material in response to +sim/highlighting/material/. */ +struct NodeHighlighting : SGPropertyChangeListener +{ + NodeHighlighting() + : + m_node(globals->get_props()->getNode("sim/highlighting/material", true /*create*/)) + { + /* Create highlight StateSet. Based on + simgear/simgear/scene/model/SGPickAnimation.cxx:sharedHighlightStateSet(). + */ + m_state_set = new osg::StateSet; + + osg::Texture2D* white = simgear::StateAttributeFactory::instance()->getWhiteTexture(); + m_state_set->setTextureAttributeAndModes( + 0, + white, + osg::StateAttribute::ON + | osg::StateAttribute::OVERRIDE + | osg::StateAttribute::PROTECTED + ); + + osg::PolygonOffset* polygonOffset = new osg::PolygonOffset; + polygonOffset->setFactor(-1); + polygonOffset->setUnits(-1); + m_state_set->setAttribute(polygonOffset, osg::StateAttribute::OVERRIDE); + m_state_set->setMode( + GL_POLYGON_OFFSET_LINE, + osg::StateAttribute::ON + | osg::StateAttribute::OVERRIDE + ); + + osg::PolygonMode* polygonMode = new osg::PolygonMode; + polygonMode->setMode( + osg::PolygonMode::FRONT_AND_BACK, + osg::PolygonMode::FILL + ); + m_state_set->setAttribute(polygonMode, osg::StateAttribute::OVERRIDE); + + osg::Material* material = new osg::Material; + material->setColorMode(osg::Material::OFF); + material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 0)); + material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 0)); + material->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 0)); + material->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 0)); + m_state_set->setAttribute( + material, + osg::StateAttribute::OVERRIDE + | osg::StateAttribute::PROTECTED + ); + // The default shader has a colorMode uniform that mimics the + // behavior of Material color mode. + + osg::Uniform* colorModeUniform = new osg::Uniform(osg::Uniform::INT, "colorMode"); + colorModeUniform->set(0); // MODE_OFF + colorModeUniform->setDataVariance(osg::Object::STATIC); + m_state_set->addUniform( + colorModeUniform, + osg::StateAttribute::OVERRIDE + | osg::StateAttribute::ON + ); + + /* Initialise highlight material properties to defaults if not already + set. */ + // XXX Alpha < 1.0 in the diffuse material value is a signal to the + // default shader to take the alpha value from the material value + // and not the glColor. In many cases the pick animation geometry is + // transparent, so the outline would not be visible without this hack. + set_rgba_default(m_node, "ambient", 0, 0, 0, 1); + set_rgba_default(m_node, "diffuse", 0.5, 0.5, 0.5, .95); + set_rgba_default(m_node, "emission", 0.75, 0.375, 0, 1); + set_rgba_default(m_node, "specular", 0.0, 0.0, 0.0, 0); + + /* Listen for changes to highlight material properties. */ + m_node->addChangeListener(this, true /*initial*/); + } + + static void set_rgba_default(SGPropertyNode* root, const std::string& path, + double red, double green, double blue, double alpha) + { + set_property_default(root, path + "/red", red); + set_property_default(root, path + "/green", green); + set_property_default(root, path + "/blue", blue); + set_property_default(root, path + "/alpha", alpha); + } + + // Sets property value if property does not exist. + static void set_property_default(SGPropertyNode* root, const std::string& path, double default_value) + { + SGPropertyNode* property = root->getNode(path, false /*create*/); + if (!property) root->setDoubleValue(path, default_value); + } + + virtual void valueChanged(SGPropertyNode* node) + { + assert(m_state_set); + osg::StateAttribute* material0 = m_state_set->getAttribute(osg::StateAttribute::MATERIAL); + osg::Material* material = dynamic_cast(material0); + assert(material); + + double red; + double green; + double blue; + double alpha; + + red = m_node->getDoubleValue("ambient/red"); + green = m_node->getDoubleValue("ambient/green"); + blue = m_node->getDoubleValue("ambient/blue"); + alpha = m_node->getDoubleValue("ambient/alpha"); + material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(red, green, blue, alpha)); + + red = m_node->getDoubleValue("diffuse/red"); + green = m_node->getDoubleValue("diffuse/green"); + blue = m_node->getDoubleValue("diffuse/blue"); + alpha = m_node->getDoubleValue("diffuse/alpha"); + material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(red, green, blue, alpha)); + + red = m_node->getDoubleValue("emission/red"); + green = m_node->getDoubleValue("emission/green"); + blue = m_node->getDoubleValue("emission/blue"); + alpha = m_node->getDoubleValue("emission/alpha"); + material->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(red, green, blue, alpha)); + + red = m_node->getDoubleValue("ambient/red"); + green = m_node->getDoubleValue("ambient/green"); + blue = m_node->getDoubleValue("ambient/blue"); + alpha = m_node->getDoubleValue("ambient/alpha"); + material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(red, green, blue, alpha)); + + red = m_node->getDoubleValue("specular/red"); + green = m_node->getDoubleValue("specular/green"); + blue = m_node->getDoubleValue("specular/blue"); + alpha = m_node->getDoubleValue("specular/alpha"); + material->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(red, green, blue, alpha)); + } + + /* Highlight/un-highlight the specified node. */ + int highlight(osg::Node* node, bool highlight) + { + int ret = 0; + osg::Group* group = node->asGroup(); + if (!group) return 0; + auto it_group_stateset = m_group_to_old_state_set.find(group); + if (it_group_stateset == m_group_to_old_state_set.end() && highlight) + { + m_group_to_old_state_set[group] = group->getStateSet(); + group->setStateSet(m_state_set.get()); + ret += 1; + } + if (it_group_stateset != m_group_to_old_state_set.end() && !highlight) + { + group->setStateSet(it_group_stateset->second); + m_group_to_old_state_set.erase(it_group_stateset); + ret += 1; + } + return ret; + } + + /* Un-highlight all nodes. */ + int clear_all() + { + int ret = 0; + for(;;) + { + if (m_group_to_old_state_set.empty()) break; + auto& group_stateset = *m_group_to_old_state_set.begin(); + highlight(group_stateset.first, false); + ret += 1; + } + return ret; + } + + ~NodeHighlighting() + { + m_node->removeChangeListener(this); + } + + SGPropertyNode_ptr m_node; + + /* We keep track of osg::StateSet's for groups that we have highlighted so + that we can turn highlighting off by swapping the original stateset back + in. */ + std::map, osg::ref_ptr> m_group_to_old_state_set; + + /* The stateset that we use to highlight nodes. */ + osg::ref_ptr m_state_set; +}; + + +Highlight::Highlight() +{ +} + +Highlight::~Highlight() +{ +} + +void Highlight::shutdown() +{ + s_property_to_info.clear(); + + s_node_to_properties.clear(); + s_dialog_to_properties.clear(); + s_keypress_to_properties.clear(); + s_menu_to_properties.clear(); + + s_menu_to_dialog.clear(); + s_dialog_to_menus.clear(); + + s_property_to_properties.clear(); + + s_property_tree_items.clear(); + s_prop_enabled.reset(); +} + +void Highlight::init() +{ + s_node_highlighting.reset(new NodeHighlighting); +} + +/* Waits until sim/fdm-initialized is true and then gets property associations +from the FDM using FGInterface::property_associations(). */ +struct FdmInitialisedListener : SGPropertyChangeListener +{ + FdmInitialisedListener() + : + m_fdm_initialised(globals->get_props()->getNode("sim/fdm-initialized", true /*create*/)) + { + m_fdm_initialised->addChangeListener(this, true /*initial*/); + } + static void property_associations_callback(void* ref, const std::string& from, const std::string& to) + { + SG_LOG(SG_GENERAL, SG_DEBUG, "fdm property association: " << from << " => " << to); + Highlight* highlight = (Highlight*) ref; + highlight->add_property_property(from, to); + } + void valueChanged(SGPropertyNode* node) override + { + if (m_fdm_initialised->getBoolValue()) + { + SG_LOG(SG_GENERAL, SG_DEBUG, "Getting property associations from FDM"); + Highlight* highlight = globals->get_subsystem(); + FDMShell* fdmshell = (FDMShell*) globals->get_subsystem("flight"); + FGInterface* fginterface = fdmshell->getInterface(); + assert(fginterface); + fginterface->property_associations(highlight, property_associations_callback); + s_fdm_initialised_listener.reset(); + } + } + ~FdmInitialisedListener() + { + globals->get_props()->getNode("sim/fdm-initialized")->removeChangeListener(this); + } + SGPropertyNode_ptr m_fdm_initialised; +}; + +void Highlight::bind() +{ + s_prop_enabled = globals->get_props()->getNode("sim/highlighting/enabled"); + globals->get_props()->setStringValue("sim/highlighting/current", ""); + globals->get_props()->setStringValue("sim/highlighting/current-ptr", "/sim/highlighting/current"); + + /* Get property associations from FDM when sim/fdm-initialized becomes true. */ + s_fdm_initialised_listener.reset(new FdmInitialisedListener); +} + +void Highlight::reinit() +{ +} + +void Highlight::unbind() +{ +} + +void Highlight::update(double dt) +{ +} + +/* Avoid ambiguities caused by possible use of "[0]" within property paths. */ +static std::string canonical(const std::string property) +{ + return simgear::strutils::replace(property, "[0]", ""); +} + + +int Highlight::highlight_nodes(osg::Node* node) +{ + if (!s_output_stats) + { + s_output_stats = true; + SG_LOG(SG_GENERAL, SG_DEBUG, "Sizes of internal maps:" + << " s_property_to_info=" << s_property_to_info.size() + << " s_node_to_properties=" << s_node_to_properties.size() + << " s_dialog_to_properties=" << s_dialog_to_properties.size() + << " s_keypress_to_properties=" << s_keypress_to_properties.size() + << " s_menu_to_properties=" << s_menu_to_properties.size() + << " s_menu_to_dialog=" << s_menu_to_dialog.size() + << " s_dialog_to_menus=" << s_dialog_to_menus.size() + << " s_property_to_properties=" << s_property_to_properties.size() + << " s_property_from_properties=" << s_property_from_properties.size() + << " s_property_tree_items=" << s_property_tree_items.size() + ); + } + if (!s_node_highlighting) return 0; + + s_node_highlighting->clear_all(); + + int num_props = 0; + + /* We build up a list of name:value items such as: + + 'dialog': joystick-config + 'menu': 0/6: File/Joystick Configuration + 'property': /controls/flight/rudder + 'property': /surface-positions/rudder-pos-norm + + These items are written into /sim/highlighting/current/. For items with + name='property', we highlight nodes which are animated by the specified + property. */ + std::set items; + + if (s_prop_enabled->getBoolValue()) + { + /* As well as itself, we also highlight other nodes by following + a limited part of the tree of property dependencies: + + property3 + -> property1 + -> + -> other nodes + -> property2 + -> other nodes + -> other nodes + -> property4 + -> other nodes + */ + for (auto& property1: Highlight::find_node_properties(node)) + { + /* animates . */ + items.insert(NameValue("property", property1)); + + for (auto& property2: Highlight::find_property_to_properties(property1)) + { + /* is set by (which animates ). */ + items.insert(NameValue("property", property2)); + } + + for (auto& property3: Highlight::find_property_from_properties(property1)) + { + /* sets (which animates ). */ + items.insert(NameValue("property", property3)); + + for (auto& property4: Highlight::find_property_to_properties(property3)) + { + /* is set by (which also + sets , which animates ). */ + items.insert(NameValue("property", property4)); + } + } + } + + /* Highlight all nodes animated by properties in , and add + nodes, dialogs, menus and keypress info to . */ + for (auto& nv: items) + { + const std::string& property = nv.value; + SG_LOG(SG_GENERAL, SG_DEBUG, "Looking at property=" << property); + const HighlightInfo& info = Highlight::find_property_info(property); + for (auto& node: info.nodes) + { + num_props += s_node_highlighting->highlight(node, true); + } + for (auto& dialog: info.dialogs) + { + items.insert(NameValue("dialog", dialog)); + for (auto& menu: Highlight::find_menu_from_dialog(dialog)) + { + items.insert(NameValue("menu", menu.description())); + } + } + for (auto& keypress: info.keypresses) + { + items.insert(NameValue("keypress", keypress)); + } + for (auto& menu: info.menus) + { + items.insert(NameValue("menu", menu.description())); + } + } + } + else + { + num_props = -1; + } + + if (items.empty()) + { + /* Create dummy item to allow property viewer to be opened on + sim/highlighting/current even if it would otherwise be currently empty. + */ + items.insert(NameValue("dummy", "")); + } + + /* Update property tree representation of currently-highlighted properties + and associated dialogs, keypresses and menus, so that it matches + . We do this by calculating what items need adding or removing from + the property tree representation */ + std::set properties_new; + std::set properties_removed; + std::set_difference( + items.begin(), items.end(), + s_property_tree_items.begin(), s_property_tree_items.end(), + std::inserter(properties_new, properties_new.begin()) + ); + std::set_difference( + s_property_tree_items.begin(), s_property_tree_items.end(), + items.begin(), items.end(), + std::inserter(properties_removed, properties_removed.begin()) + ); + SGPropertyNode* current = globals->get_props()->getNode("sim/highlighting/current", true /*create*/); + for (int pos=0; posnChildren(); ++pos) + { + SGPropertyNode* child = current->getChild(pos); + auto nv = NameValue(child->getNameString(), child->getStringValue()); + if (properties_removed.find(nv) != properties_removed.end()) + { + current->removeChild(pos); + pos -= 1; + } + } + for (auto& name_value: properties_new) + { + current->addChild(name_value.name)->setStringValue(name_value.value); + } + + /* Remember the current items for next time. */ + std::swap(items, s_property_tree_items); + + return num_props; +} + +/* Functions that interrogate our internal data. + + +We use these to avoid adding multiple empty items to our std::maps. +*/ +static const HighlightInfo info_empty; +static const std::set set_string_empty; +static const std::set set_menu_empty; + +const HighlightInfo& Highlight::find_property_info(const std::string& property) +{ + std::string property2 = canonical(property); + auto it = s_property_to_info.find(property2); + if (it == s_property_to_info.end()) return info_empty; + return it->second; +} + +const std::set& Highlight::find_node_properties(osg::Node* node) +{ + auto it = s_node_to_properties.find(node); + if (it == s_node_to_properties.end()) return set_string_empty; + return it->second; +} + +const std::set& Highlight::find_dialog_properties(const std::string& dialog) +{ + auto it = s_dialog_to_properties.find(dialog); + if (it == s_dialog_to_properties.end()) return set_string_empty; + return it->second; +} + +const std::set& Highlight::find_keypress_properties(const std::string& keypress) +{ + auto it = s_keypress_to_properties.find(keypress); + if (it == s_keypress_to_properties.end()) return set_string_empty; + return it->second; +} + +const std::set& Highlight::find_menu_properties(const HighlightMenu& menu) +{ + auto it = s_menu_to_properties.find(menu); + if (it == s_menu_to_properties.end()) return set_string_empty; + return it->second; +} + +const std::set& Highlight::find_property_to_properties(const std::string& property) +{ + std::string property2 = canonical(property); + auto it = s_property_to_properties.find(property2); + if (it == s_property_to_properties.end()) return set_string_empty; + return it->second; +} + +const std::set& Highlight::find_property_from_properties(const std::string& property) +{ + std::string property2 = canonical(property); + auto it = s_property_from_properties.find(property2); + if (it == s_property_from_properties.end()) return set_string_empty; + return it->second; +} + +const std::set& Highlight::find_menu_from_dialog(const std::string& dialog) +{ + auto it = s_dialog_to_menus.find(dialog); + if (it == s_dialog_to_menus.end()) return set_menu_empty; + return it->second; +} + + +/* Functions that populate our internal data. */ + +void Highlight::add_property_node(const std::string& property, osg::ref_ptr node) +{ + std::string property2 = canonical(property); + s_property_to_info[property2].nodes.insert(node); + if (s_node_to_properties[node].insert(property2).second) + SG_LOG(SG_INPUT, SG_DEBUG, "node=" << node.get() << " property=" << property2); +} + +void Highlight::add_property_dialog(const std::string& property, const std::string& dialog) +{ + std::string property2 = canonical(property); + s_property_to_info[property2].dialogs.insert(dialog); + if (s_dialog_to_properties[dialog].insert(property2).second) + SG_LOG(SG_INPUT, SG_DEBUG, "dialog=" << dialog << " property=" << property2); +} + +void Highlight::add_property_keypress(const std::string& property, const std::string& keypress) +{ + std::string property2 = canonical(property); + s_property_to_info[property2].keypresses.insert(keypress); + if (s_keypress_to_properties[keypress].insert(property2).second) + SG_LOG(SG_INPUT, SG_DEBUG, "keypress=" << keypress << " property=" << property2); +} + +void Highlight::add_property_menu(HighlightMenu menu, const std::string& property) +{ + std::string property2 = canonical(property); + s_property_to_info[property2].menus.insert(menu); + if (s_menu_to_properties[menu].insert(property2).second) + SG_LOG(SG_INPUT, SG_DEBUG, "menu=(" << menu.menu << " " << menu.item << ") property=" << property2); +} + +void Highlight::add_menu_dialog(HighlightMenu menu, const std::string& dialog) +{ + s_menu_to_dialog[menu] = dialog; + if (s_dialog_to_menus[dialog].insert(menu).second) + SG_LOG(SG_INPUT, SG_DEBUG, "menu (" << menu.menu << " " << menu.item << ") dialog=" << dialog); +} + +void Highlight::add_property_property(const std::string& from0, const std::string& to0) +{ + std::string from = canonical(from0); + std::string to = canonical(to0); + if (from == to) return; + bool is_new = false; + if (s_property_to_properties[from].insert(to).second) is_new = true; + if (s_property_from_properties[to].insert(from).second) is_new = true; + if (is_new) + { + // Add transitive associations. + for (auto& toto: s_property_to_properties[to]) + { + Highlight::add_property_property(from, toto); + } + for (auto& fromfrom: s_property_from_properties[from]) + { + Highlight::add_property_property(fromfrom, to); + } + } +} diff --git a/src/GUI/Highlight.hxx b/src/GUI/Highlight.hxx new file mode 100644 index 000000000..38591bb10 --- /dev/null +++ b/src/GUI/Highlight.hxx @@ -0,0 +1,114 @@ +#pragma once + +#include +#include + +#include +#include + +/* Represents a menu item. */ +struct HighlightMenu +{ + HighlightMenu(int menu, int item) : menu(menu), item(item) + {} + int menu; /* Number of menu in the menubar, e.g. 0 is 'File'. */ + int item; /* Number of item in the menu. */ + std::string description() const; /* E.g. "0/6: File/Joystick Configuration". */ +}; + +/* Information about OSG nodes, dialogs etc that are associated with a +particular property. */ +struct HighlightInfo +{ + /* OSG Nodes that are animated by the property. */ + std::set> nodes; + + /* Dialogues that modify the property.*/ + std::set dialogs; + + /* Keypresses that modify the property. */ + std::set keypresses; + + /* Menu items that modify the property. */ + std::set menus; +}; + +struct Highlight : SGSubsystem +{ + Highlight(); + ~Highlight(); + + void bind() override; + void init() override; + void reinit() override; + void shutdown() override; + void unbind() override; + void update(double dt) override; + + static const char* staticSubsystemClassId() { return "reflect"; } + + /* If specified node is animated, highlights it and other nodes + that are animated by the same or related properties. Also updates + /sim/highlighting/current to contain information about associated dialogs, + menus, and keypresses. + + Returns the number of properties found. Returns -1 if highlighting is not + currently enabled. */ + int highlight_nodes(osg::Node* node); + + /* Returns information about nodes and UI elements that are associated with a + specific property. */ + const HighlightInfo& find_property_info(const std::string& property); + + + /* Below are individual functions that return properties that are associated + with a particular node or UI element. */ + + /* Returns list of properties that are used to animate the specified OSG node. + */ + const std::set& find_node_properties(osg::Node* node); + + /* Returns list of properties affected by specified dialog. */ + const std::set& find_dialog_properties(const std::string& dialog); + + /* Returns list of properties affected by specified keypress. */ + const std::set& find_keypress_properties(const std::string& keypress); + + /* Returns list of properties affected by specified menu. */ + const std::set& find_menu_properties(const HighlightMenu& menu); + + /* Returns list of properties that are influenced by the specified property, + /e.g. if is controls/flight/rudder, the returned set could contain + /surface-positions/rudder-pos-norm. */ + const std::set& find_property_to_properties(const std::string& property); + + /* Returns list of properties that influence the specified property, e.g. + if is /surface-positions/rudder-pos-norm, the returned set could + contain /controls/flight/rudder. */ + const std::set& find_property_from_properties(const std::string& property); + + /* Returns list of menus that open the specified dialog. */ + const std::set& find_menu_from_dialog(const std::string& dialog); + + + /* Below are functions that are used to set up associations. */ + + /* Should be called if is animated using . */ + void add_property_node(const std::string& property, osg::ref_ptr node); + + /* Should be called if affects . */ + void add_property_dialog(const std::string& property, const std::string& dialog); + + /* Should be called if affects . */ + void add_property_keypress(const std::string& property, const std::string& keypress); + + /* Should be called if affects . */ + void add_property_menu(HighlightMenu menu, const std::string& property); + + /* Should be called if opens . */ + void add_menu_dialog(HighlightMenu menu, const std::string& dialog); + + /* Should be called if two properties are associated, for example YASim + associates /controls/flight/flaps with /surface-positions/flap-pos-norm. */ + void add_property_property(const std::string& property1, const std::string& property2); +}; diff --git a/src/GUI/new_gui.cxx b/src/GUI/new_gui.cxx index c54e7b2ac..c0d0bff3b 100644 --- a/src/GUI/new_gui.cxx +++ b/src/GUI/new_gui.cxx @@ -39,6 +39,8 @@ #include "FGFontCache.hxx" #include "FGColor.hxx" +#include "Highlight.hxx" + // ignore the word Navaid here, it's a DataCache #include @@ -64,6 +66,44 @@ NewGUI::~NewGUI () delete it->second; } +// Recursively finds all nodes called and appends the string values of +// these nodes to . +// +static void findAllLeafValues(SGPropertyNode* node, const std::string& leaf, std::vector& out) +{ + const char* name = node->getName(); + if (name == leaf) { + out.push_back(node->getStringValue()); + } + for (int i=0; inChildren(); ++i) { + findAllLeafValues(node->getChild(i), leaf, out); + } +} + + +/* Registers menu-dialog associations with Highlight::add_menu_dialog(). */ +static void scanMenus() +{ + /* We find all nodes called 'dialog-name' in + sim/menubar/default/menu[]/item[]. */ + SGPropertyNode* menubar = globals->get_props()->getNode("sim/menubar/default"); + assert(menubar); + Highlight* highlight = globals->get_subsystem(); + for (int menu_p=0; menu_pnChildren(); ++menu_p) { + SGPropertyNode* menu = menubar->getChild(menu_p); + if (menu->getNameString() != "menu") continue; + for (int item_p=0; item_pnChildren(); ++item_p) { + SGPropertyNode* item = menu->getChild(item_p); + if (item->getNameString() != "item") continue; + std::vector dialog_names; + findAllLeafValues(item, "dialog-name", dialog_names); + for (auto dialog_name: dialog_names) { + highlight->add_menu_dialog(HighlightMenu(menu->getIndex(), item->getIndex()), dialog_name); + } + } + } +} + void NewGUI::init () { @@ -96,6 +136,7 @@ NewGUI::init () // Fix for http://code.google.com/p/flightgear-bugs/issues/detail?id=947 fgGetNode("sim/menubar")->setAttribute(SGPropertyNode::PRESERVE, true); _menubar->init(); + scanMenus(); } void @@ -384,6 +425,7 @@ NewGUI::newDialog (SGPropertyNode* props) } } + void NewGUI::readDir (const SGPath& path) { @@ -396,7 +438,37 @@ NewGUI::readDir (const SGPath& path) flightgear::NavDataCache* cache = flightgear::NavDataCache::instance(); flightgear::NavDataCache::Transaction txn(cache); + Highlight* highlight = globals->get_subsystem(); for (SGPath xmlPath : dir.children(simgear::Dir::TYPE_FILE, ".xml")) { + + SGPropertyNode_ptr props = new SGPropertyNode; + SGPropertyNode *nameprop = nullptr; + std::string name; + + /* Always parse the dialog even if cache->isCachedFileModified() says + it is cached, so that we can register property-dialog associations with + Highlight::add_property_dialog(). */ + try { + readProperties(xmlPath, props); + } catch (const sg_exception &) { + SG_LOG(SG_INPUT, SG_ALERT, "Error parsing dialog " << xmlPath); + props.reset(); + } + if (props) { + nameprop = props->getNode("name"); + if (nameprop) { + name = nameprop->getStringValue(); + // Look for 'property' nodes within the dialog. (could + // also look for "dialog-name" to catch things like + // dialog-show.) + std::vector property_paths; + findAllLeafValues(props, "property", property_paths); + for (auto property_path: property_paths) { + highlight->add_property_dialog(property_path, name); + } + } + } + if (!cache->isCachedFileModified(xmlPath)) { // cached, easy string name = cache->readStringProperty(xmlPath.utf8Str()); @@ -404,32 +476,27 @@ NewGUI::readDir (const SGPath& path) continue; } - // we need to parse the actual XML - SGPropertyNode_ptr props = new SGPropertyNode; - try { - readProperties(xmlPath, props); - } catch (const sg_exception &) { + // we need to parse the actual XML + if (!props) { SG_LOG(SG_INPUT, SG_ALERT, "Error parsing dialog " << xmlPath); continue; } - SGPropertyNode *nameprop = props->getNode("name"); if (!nameprop) { SG_LOG(SG_INPUT, SG_WARN, "dialog " << xmlPath << " has no name; skipping."); continue; } - string name = nameprop->getStringValue(); _dialog_names[name] = xmlPath; - // update cached values - if (!cache->isReadOnly()) { - cache->stampCacheFile(xmlPath); - cache->writeStringProperty(xmlPath.utf8Str(), name); - } + // update cached values + if (!cache->isReadOnly()) { + cache->stampCacheFile(xmlPath); + cache->writeStringProperty(xmlPath.utf8Str(), name); + } } // of directory children iteration txn.commit(); -} +} //////////////////////////////////////////////////////////////////////// // Style handling. //////////////////////////////////////////////////////////////////////// diff --git a/src/GUI/property_list.cxx b/src/GUI/property_list.cxx index a25fa9edb..26f8e27f2 100644 --- a/src/GUI/property_list.cxx +++ b/src/GUI/property_list.cxx @@ -157,18 +157,18 @@ static void sanitize(stdString& s) -PropertyList::PropertyList(int minx, int miny, int maxx, int maxy, SGPropertyNode *start) : +PropertyList::PropertyList(int minx, int miny, int maxx, int maxy, SGPropertyNode *start, bool readonly) : puaList(minx, miny, maxx, maxy, short(0), 20), GUI_ID(FGCLASS_PROPERTYLIST), _curr(start), _return(0), _entries(0), _num_entries(0), - _verbose(false) - + _verbose(false), + _readonly(readonly) { _list_box->setUserData(this); - _list_box->setCallback(handle_select); + if (!_readonly) _list_box->setCallback(handle_select); _list_box->setValue(0); update(); } @@ -265,7 +265,7 @@ void PropertyList::update(bool restore_pos) _num_entries = (int)_curr->nChildren(); // instantiate string objects and add [.] and [..] for subdirs - if (!_curr->getParent()) { + if (_readonly || !_curr->getParent()) { _entries = new char*[_num_entries + 1]; pi = 0; _dot_files = false; diff --git a/src/GUI/property_list.hxx b/src/GUI/property_list.hxx index e11d5bfe8..8e1780fb2 100644 --- a/src/GUI/property_list.hxx +++ b/src/GUI/property_list.hxx @@ -32,7 +32,9 @@ class PropertyList : public puaList, public SGPropertyChangeListener, public GUI_ID { public: - PropertyList(int minx, int miny, int maxx, int maxy, SGPropertyNode *); + // If is true, we don't show . or .. items and don't respond to + // mouse clicks or keypresses. + PropertyList(int minx, int miny, int maxx, int maxy, SGPropertyNode *root, bool readonly); ~PropertyList(); void update (bool restore_slider_pos = false); @@ -89,6 +91,7 @@ private: bool _dot_files; // . and .. pseudo-dirs currently shown? bool _verbose; // show SGPropertyNode flags std::string _return_path; + bool _readonly; }; diff --git a/src/Main/fg_init.cxx b/src/Main/fg_init.cxx index f6b39b9da..0baae79cc 100755 --- a/src/Main/fg_init.cxx +++ b/src/Main/fg_init.cxx @@ -100,6 +100,7 @@ #include #include #include +#include #include #include #include @@ -988,6 +989,7 @@ void fgCreateSubsystems(bool duringReset) { // Initialize the property interpolator subsystem. Put into the INIT // group because the "nasal" subsystem may need it at GENERAL take-down. globals->add_subsystem("prop-interpolator", new FGInterpolator, SGSubsystemMgr::INIT); + globals->add_new_subsystem(SGSubsystemMgr::INIT); globals->add_subsystem("gui", new NewGUI, SGSubsystemMgr::INIT); } diff --git a/src/Model/acmodel.cxx b/src/Model/acmodel.cxx index 2e0ab6ee2..fed8ae9f1 100644 --- a/src/Model/acmodel.cxx +++ b/src/Model/acmodel.cxx @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -26,6 +27,7 @@ #include #include #include +#include #include "acmodel.hxx" @@ -66,6 +68,80 @@ FGAircraftModel::~FGAircraftModel () shutdown(); } + +// Gathers information about all animated nodes and the properties that they +// depend on, and registers with Highlight::add_property_node(). +// +struct VisitorHighlight : osg::NodeVisitor +{ + VisitorHighlight() + { + m_highlight = globals->get_subsystem(); + setTraversalMode(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN); + } + std::string spaces() + { + return std::string(m_level*4, ' '); + } + virtual void apply(osg::Node& node) + { + SG_LOG(SG_GENERAL, SG_DEBUG, spaces() << "node: " << node.libraryName() << "::" << node.className()); + m_level += 1; + traverse(node); + m_level -= 1; + } + virtual void apply(osg::Group& group) + { + // Parent nodes of are animated by properties in + // , so register the association between and + // these properties. + SG_LOG(SG_GENERAL, SG_DEBUG, spaces() << "group: " << group.libraryName() << "::" << group.className()); + for (auto name: m_highlight_names) + { + m_highlight->add_property_node(name, &group); + } + m_level += 1; + traverse(group); + m_level -= 1; + } + virtual void apply( osg::Transform& node ) + { + SG_LOG(SG_GENERAL, SG_DEBUG, spaces() << "transform: " + << node.libraryName() << "::" << node.className() + << ": " << typeid(node).name() + ); + + SGSharedPtr expression = TransformExpression(&node); + int num_added = 0; + if (expression) { + // All 's child nodes will be affected by so + // add all of 's properties to so + // that we can call Highlight::add_property_node() for each child + // node. + std::set properties; + expression->collectDependentProperties(properties); + SG_LOG(SG_GENERAL, SG_DEBUG, spaces() << typeid(node).name() << ":"); + for (auto p: properties) { + SG_LOG(SG_GENERAL, SG_DEBUG, spaces() << " " << p->getPath(true)); + num_added += 1; + m_highlight_names.push_back(p->getPath(true /*simplify*/)); + } + } + + m_level += 1; + traverse(node); + m_level -= 1; + + // Remove the names we appended to earlier. + assert((int) m_highlight_names.size() >= num_added); + m_highlight_names.resize(m_highlight_names.size() - num_added); + } + unsigned int m_level = 0; // Only used to indent diagnostics. + std::vector m_highlight_names; + Highlight* m_highlight; +}; + + void FGAircraftModel::init () { @@ -142,6 +218,10 @@ FGAircraftModel::init () node = _interior->getSceneGraph(); globals->get_scenery()->get_interior_branch()->addChild(node); } + + // Register animated nodes and associated properties with Highlight. + VisitorHighlight visitor_highlight; + visitor_highlight.traverse(*node); } void diff --git a/src/Model/acmodel.hxx b/src/Model/acmodel.hxx index 96745ba20..05a477c99 100644 --- a/src/Model/acmodel.hxx +++ b/src/Model/acmodel.hxx @@ -12,6 +12,7 @@ #include #include // for SGSubsystem +#include // Don't pull in the headers, since we don't need them here. diff --git a/src/Viewer/renderer.cxx b/src/Viewer/renderer.cxx index 419c01697..d2489fcbd 100644 --- a/src/Viewer/renderer.cxx +++ b/src/Viewer/renderer.cxx @@ -94,6 +94,7 @@ #include #include #include +#include #include #include @@ -755,6 +756,7 @@ FGRenderer::setupView( void ) } // Update all Visuals (redraws anything graphics related) +// Called every frame. void FGRenderer::update( ) { if (!_position_finalized || !_scenery_loaded->getBoolValue()) @@ -994,6 +996,12 @@ PickList FGRenderer::pick(const osg::Vec2& windowPos) if (!computeIntersections(CameraGroup::getDefault(), windowPos, intersections)) return result; + + // We attempt to highlight nodes until Highlight::highlight_nodes() + // succeeds and returns +ve, or highlighting is disabled and it returns -1. + Highlight* highlight = globals->get_subsystem(); + int higlight_num_props = 0; + for (Intersections::iterator hit = intersections.begin(), e = intersections.end(); hit != e; @@ -1002,6 +1010,9 @@ PickList FGRenderer::pick(const osg::Vec2& windowPos) osg::NodePath::const_reverse_iterator npi; for (npi = np.rbegin(); npi != np.rend(); ++npi) { + if (!higlight_num_props) { + higlight_num_props = highlight->highlight_nodes(*npi); + } SGSceneUserData* ud = SGSceneUserData::getSceneUserData(*npi); if (!ud || (ud->getNumPickCallbacks() == 0)) continue;