FlightGear next
PUICompatObject.cxx
Go to the documentation of this file.
1// PUICompatObject.cxx - XML dialog object without using PUI
2// Copyright (C) 2022 James Turner
3// SPDX-License-Identifier: GPL-2.0-or-later
4
5#include "config.h"
6
7#include "PUICompatObject.hxx"
8
9#include <algorithm>
10
11#include <simgear/misc/strutils.hxx>
12#include <simgear/props/props.hxx>
13#include <simgear/props/props_io.hxx> // for copyProperties
14
16#include <GUI/new_gui.hxx>
17#include <Main/fg_props.hxx>
20#include <string>
21
22using namespace std::string_literals;
23namespace strutils = simgear::strutils;
24
25extern naRef propNodeGhostCreate(naContext c, SGPropertyNode* n);
26
27PUICompatObject::PUICompatObject(naRef impl, const std::string& type)
28 : nasal::Object(impl), _type(type)
29{
30}
31
33{
34 auto labelNode = _config->getChild("label");
35 if (labelNode) {
36 labelNode->removeChangeListener(this);
37 }
38
39 if (_value) {
40 _value->removeChangeListener(this);
41 }
42}
43
44naRef f_makeCompatObjectPeer(const nasal::CallContext& ctx)
45{
46 return ctx.to_nasal(SGSharedPtr<PUICompatObject>(
47 new PUICompatObject(ctx.requireArg<naRef>(0), ctx.requireArg<std::string>(1))));
48}
49
50naRef f_translateString(const PUICompatObject& widget, nasal::CallContext ctx)
51{
52 const auto key = ctx.requireArg<std::string>(0);
53 const auto resource = ctx.getArg<std::string>(1);
54 const auto domain = ctx.getArg<std::string>(2);
55 return ctx.to_nasal(widget.translateString(key, resource, domain));
56}
57
58naRef f_translatePluralString(const PUICompatObject& widget, nasal::CallContext ctx)
59{
60 const auto key = ctx.requireArg<std::string>(0);
61 const auto cardinal = ctx.requireArg<int>(1);
62 const auto resource = ctx.getArg<std::string>(2);
63 const auto domain = ctx.getArg<std::string>(3);
64 return ctx.to_nasal(
65 widget.translatePluralString(key, cardinal, resource, domain));
66}
67
68void PUICompatObject::setupGhost(nasal::Hash& compatModule)
69{
70 using NasalGUIObject = nasal::Ghost<SGSharedPtr<PUICompatObject>>;
71 NasalGUIObject::init("gui.xml.CompatObject")
72 .bases<nasal::ObjectRef>()
73 .member("config", &PUICompatObject::config)
74 .method("configValue", &PUICompatObject::nasalGetConfigValue)
75 .member("value", &PUICompatObject::propertyValue)
76 .member("property", &PUICompatObject::property)
77 .member("geometry", &PUICompatObject::geometry)
78 .member("x", &PUICompatObject::getX)
79 .member("y", &PUICompatObject::getY)
80 .member("width", &PUICompatObject::width)
81 .member("height", &PUICompatObject::height)
82 .member("children", &PUICompatObject::children)
83 .member("dialog", &PUICompatObject::dialog)
84 .member("name", &PUICompatObject::name)
85 .member("parent", &PUICompatObject::parent)
86 .member("live", &PUICompatObject::isLive)
89 .member("type", &PUICompatObject::type)
90 .member("radioGroup", &PUICompatObject::radioGroupIdent)
91 .member("hasBindings", &PUICompatObject::hasBindings)
92 .method("show", &PUICompatObject::show)
93 .method("activateBindings", &PUICompatObject::activateBindings)
94 .method("gridLocation", &PUICompatObject::gridLocation)
95 .method("trN", f_translatePluralString)
96 .method("tr", f_translateString);
97
98 nasal::Hash objectHash = compatModule.createHash("Object");
99 objectHash.set("new", &f_makeCompatObjectPeer);
100}
101
102PUICompatObjectRef PUICompatObject::createForType(const std::string& type, SGPropertyNode_ptr config)
103{
104 auto nas = globals->get_subsystem<FGNasalSys>();
105 nasal::Context ctx;
106
107 nasal::Hash guiModule{nas->getModule("gui"), ctx};
108 if (guiModule.isNil()) {
109 throw sg_exception("Can't initialize PUICompat Nasal");
110 }
111
112 auto f = guiModule.get<std::function<PUICompatObjectRef(std::string)>>("_createCompatObject");
113 PUICompatObjectRef object = f(type);
114 // set config
115 object->_config = config;
116 return object;
117}
118
120{
121 const auto uiVersion = dialog()->uiVersion();
122
123 _name = _config->getStringValue("name");
124 _label = _config->getStringValue("label");
125 bool isLive = _config->getBoolValue("live");
126
127 int parentWidth = 800;
128 int parentHeight = 600;
129
130 //bool presetSize = _config->hasValue("width") && _config->hasValue("height");
131 int width = _config->getIntValue("width", parentWidth);
132 int height = _config->getIntValue("height", parentHeight);
133 int x = _config->getIntValue("x", (parentWidth - width) / 2);
134 int y = _config->getIntValue("y", (parentHeight - height) / 2);
135
136 setGeometry(SGRectd{static_cast<double>(x), static_cast<double>(y),
137 static_cast<double>(width), static_cast<double>(height)});
138
139 if (_config->hasChild("visible")) {
140 _visibleCondition = sgReadCondition(globals->get_props(), _config->getChild("visible"));
141 }
142
143 if (_config->hasChild("enable")) {
144 _enableCondition = sgReadCondition(globals->get_props(), _config->getChild("enable"));
145 }
146
147 if (_config->hasChild("label")) {
148 _config->getChild("label")->addChangeListener(this);
149 }
150
151 if (_config->hasValue("property")) {
152 _value = fgGetNode(_config->getStringValue("property"), true);
153
154 if (isLive) {
155 _live = LiveValueMode::Listener;
156 if (_value->isTied() || _value->isAlias()) {
157 const auto isSafe = _value->getAttribute(SGPropertyNode::LISTENER_SAFE);
158 if (!isSafe) {
159 SG_LOG(SG_GUI, SG_DEV_WARN, "Requested live updating of unsafe tied property: " << _value->getPath() << "; please fix this propertty to be non-tied or make it listener-safe explicitly.");
160
161 // we are kind, support polled mode for now
162 _live = LiveValueMode::Polled;
163 }
164 }
165
166 if (_live == LiveValueMode::Listener) {
167 _value->addChangeListener(this);
168 }
169 }
170 }
171
172 // parse version 2 featrues
173 if (uiVersion >= 2) {
174 if (_type == "radio") {
175 auto g = _config->getStringValue("radio-group");
176 if (g.empty()) {
177 SG_LOG(SG_GUI, SG_DEV_WARN, "UIv2 radio button does not specify a group ID (at " << _config->getLocation() << ")");
178 }
179 }
180 }
181
182 const auto bindings = _config->getChildren("binding");
183 if (!bindings.empty()) {
184 for (auto bindingNode : bindings) {
185 const auto cmd = bindingNode->getStringValue("command");
186 if (cmd == "nasal") {
187 // we need to clone the binding node, so we can unique the
188 // Nasal module. Otherwise we always modify the global dialog
189 // definition, and cloned dialogs use the same Nasal module for
190 // <nasal> bindings, which goes wrong. (Especially, the property
191 // inspector)
192
193 // memory ownership works because SGBinding has a ref to its
194 // argument node and holds onto it.
195 SGPropertyNode_ptr copiedBinding = new SGPropertyNode;
196 copyProperties(bindingNode, copiedBinding);
197 copiedBinding->setStringValue("module", dialog()->nasalModule());
198
199 bindingNode = copiedBinding;
200 }
201
202 _bindings.push_back(new SGBinding(bindingNode, globals->get_props()));
203 }
204 }
205
206 // children
207 int nChildren = _config->nChildren();
208 for (int i = 0; i < nChildren; i++) {
209 auto childNode = _config->getChild(i);
210
211 const auto nodeName = childNode->getNameString();
212 if (!isNodeAChildObject(nodeName, uiVersion)) {
213 continue;
214 }
215
216 SGSharedPtr<PUICompatObject> childObject = createForType(nodeName, childNode);
217 childObject->_parent = this;
218 _children.push_back(childObject);
219 }
220
221 auto nas = globals->get_subsystem<FGNasalSys>();
222 callMethod<void>("init", nas->wrappedPropsNode(_config));
223
224 // recursively init children
225 for (auto c : _children) {
226 c->init();
227 }
228
229 callMethod<void>("postinit");
230}
231
233{
234 const auto uiVersion = dialog()->uiVersion();
235 if (uiVersion < 2) {
236 throw std::runtime_error("radioGroupIdent: Not allowed at UI version < 2");
237 }
238
239 return _config->getStringValue("radio-group");
240}
241
242naRef PUICompatObject::show(naRef viewParent)
243{
244 nasal::Context ctx;
245 return callMethod<naRef>("show", viewParent);
246}
247
248bool PUICompatObject::isNodeAChildObject(const std::string& nm, int uiVersion)
249{
250 string_list typeNames = {
251 "button", "one-shot", "slider", "dial",
252 "text", "input", "radio",
253 "combo", "textbox", "select",
254 "hrule", "vrule", "group", "frame",
255 "checkbox"};
256
257 if (uiVersion >= 2) {
258 typeNames.push_back("standard-button");
259 typeNames.push_back("tabs");
260 typeNames.push_back("button-box");
261 }
262
263 auto it = std::find(typeNames.begin(), typeNames.end(), nm);
264 return it != typeNames.end();
265}
266
268{
269 if (_enableCondition) {
270 const bool e = _enableCondition->test();
271 if (e != _enabled) {
272 _enabled = e;
273 callMethod<void, bool>("enabledChanged", e);
274 }
275 }
276
277 if (_visibleCondition) {
278 const bool e = _visibleCondition->test();
279 if (e != _visible) {
280 _visible = e;
281 callMethod<void, bool>("visibleChanged", e);
282 }
283 }
284
285 if (_labelChanged) {
286 _labelChanged = false;
287 callMethod<void, std::string>("labelChanged", _label);
288 }
289
290 if (_value) {
291 if (_live == LiveValueMode::Polled) {
292 // this is a bit heavy, especailly for double-valued numerical
293 // properties. Lets's see how it goes.
294 const auto nv = _value->getStringValue();
295 if (nv != _oldPolledValue) {
296 _valueChanged = true;
297 _oldPolledValue = nv;
298 }
299 }
300
301 if (_valueChanged) {
302 _valueChanged = false;
303 callMethod<void>("valueChanged");
304 }
305 }
306}
307
309{
310 if (!_value) {
311 return;
312 }
313
314 if (_live != LiveValueMode::OnApply) {
315 return;
316 }
317
318 // avoid updates where the value didn't actually change
319 const auto nv = _value->getStringValue();
320 if (nv != _oldPolledValue) {
321 _valueChanged = true;
322 _oldPolledValue = nv;
323 }
324
325 // we don't call update here(), it will hapen next cycle.
326}
327
329{
330 callMethod<void>("apply");
331 if (_live == LiveValueMode::OnApply) {
332 _valueChanged = false;
333 }
334}
335
337{
338 if (!_value)
339 return naNil();
340
341 auto nas = globals->get_subsystem<FGNasalSys>();
342 return nas->wrappedPropsNode(_value.get());
343}
344
345naRef PUICompatObject::propertyValue(naContext ctx) const
346{
347 return FGNasalSys::getPropertyValue(ctx, _value);
348}
349
350
352{
353 auto nas = globals->get_subsystem<FGNasalSys>();
354 return nas->wrappedPropsNode(_config.get());
355}
356
357naRef PUICompatObject::nasalGetConfigValue(const nasal::CallContext ctx) const
358{
359 auto name = ctx.requireArg<std::string>(0);
360 naRef defaultVal = ctx.getArg(1, naNil());
361 SGPropertyNode_ptr nd = _config->getChild(name);
362 if (!nd || !nd->hasValue())
363 return defaultVal;
364
365 return FGNasalSys::getPropertyValue(ctx.c_ctx(), nd.get());
366}
367
368void PUICompatObject::valueChanged(SGPropertyNode* node)
369{
370 if (node->getNameString() == "label") {
371 _labelChanged = true;
372 _label = node->getStringValue();
373 return;
374 }
375
376 if (_live == LiveValueMode::OnApply)
377 return;
378
379 // don't fire Nasal callback now, might cause recursion
380 _valueChanged = true;
381}
382
384{
385 if (!_enabled) {
386 SG_LOG(SG_GUI, SG_DEV_ALERT, "Skipping binding activation for disabled widget:" << name());
387 return;
388 }
389
390 auto guiSub = globals->get_subsystem<NewGUI>();
391 assert(guiSub);
392
393 guiSub->setActiveDialog(dialog());
394 fireBindingList(_bindings);
395 guiSub->setActiveDialog(nullptr);
396}
397
399{
400 return !_bindings.empty();
401}
402
403void PUICompatObject::setGeometry(const SGRectd& g)
404{
406}
407
408void PUICompatObject::updateGeometry(const SGRectd& newGeom)
409{
410 if (newGeom == _geometry) {
411 return;
412 }
413
414 _geometry = newGeom;
415 callMethod<void>("geometryChanged");
416
417 // cascade to children?
418}
419
421{
422 return _geometry.pos().x();
423}
424
426{
427 return _geometry.pos().y();
428}
429
431{
432 return _geometry.width();
433}
434
436{
437 return _geometry.height();
438}
439
441{
442 return _geometry;
443}
444
446{
447 return _parent.lock();
448}
449
451{
452 return _children;
453}
454
456{
457 if (_visibleCondition) {
458 return _visibleCondition->test();
459 }
460
461 return _visible;
462}
463
465{
466 if (_enableCondition) {
467 return _enableCondition->test();
468 }
469
470 return _enabled;
471}
472
473const std::string& PUICompatObject::type() const
474{
475 return _type;
476}
477
479{
480 if (_visibleCondition) {
481 SG_LOG(SG_GUI, SG_DEV_ALERT, "Trying to set visiblity on widget with visible condition already defined");
482 return;
483 }
484
485 if (_visible == v)
486 return;
487
488 _visible = v;
489 callMethod<void, bool>("visibleChanged", _visible);
490}
491
493{
494 if (_enableCondition) {
495 SG_LOG(SG_GUI, SG_DEV_ALERT, "Trying to set enabled on widget with enable condition already defined");
496 return;
497 }
498
499 if (_enabled == e)
500 return;
501
502 _enabled = e;
503 callMethod<void, bool>("enabledChanged", _enabled);
504}
505
507{
508 if (name == _name) {
509 return PUICompatObjectRef(const_cast<PUICompatObject*>(this));
510 }
511
512 for (auto child : _children) {
513 auto r = child->widgetByName(name);
514 if (r) {
515 return r;
516 }
517 }
518
519 return {};
520}
521
522void PUICompatObject::recursiveUpdate(const std::string& objectName)
523{
524 if (objectName.empty() || (objectName == _name)) {
525 update();
526 }
527
528 for (auto child : _children) {
529 child->recursiveUpdate(objectName);
530 }
531}
532
533void PUICompatObject::recursiveUpdateValues(const std::string& objectName)
534{
535 if (objectName.empty() || (objectName == _name)) {
536 updateValue();
537 }
538
539 for (auto child : _children) {
540 child->recursiveUpdateValues(objectName);
541 }
542}
543
544
545void PUICompatObject::recursiveApply(const std::string& objectName)
546{
547 if (objectName.empty() || (objectName == _name)) {
548 apply();
549 }
550
551 for (auto child : _children) {
552 child->recursiveApply(objectName);
553 }
554}
555
556void PUICompatObject::recursiveOnDelete()
557{
558 // bottom up call of del()
559 for (auto child : _children) {
560 child->recursiveOnDelete();
561 }
562
563 callMethod<void>("del");
564}
565
566void PUICompatObject::setDialog(PUICompatDialogRef dialog)
567{
568 _dialog = dialog;
569}
570
572{
573 PUICompatObjectRef pr = _parent.lock();
574 if (pr) {
575 return pr->dialog();
576 }
577
578 return _dialog.lock();
579}
580
581nasal::Hash PUICompatObject::gridLocation(const nasal::CallContext& ctx) const
582{
583 nasal::Hash result{ctx.c_ctx()};
584 result.set("column", _config->getIntValue("col"));
585 result.set("row", _config->getIntValue("row"));
586 result.set("columnSpan", _config->getIntValue("colspan", 1));
587 result.set("rowSpan", _config->getIntValue("rowspan", 1));
588 return result;
589}
590
591std::string PUICompatObject::translatePluralString(const std::string& key, int cardinal, const std::string& resource, const std::string& domain) const
592{
593 auto strippedKey = strutils::strip(key);
594 auto res = resource.empty() ? "dialog-"s + dialog()->getName() : resource;
595 auto dom = domain.empty() ? dialog()->translationDomain() : domain;
596 return FGTranslate(dom).getPluralWithDefault(cardinal, res, strippedKey,
597 strippedKey);
598}
599
600std::string PUICompatObject::translateString(const std::string& key, const std::string& resource, const std::string& domain) const
601{
602 auto strippedKey = strutils::strip(key);
603 auto res = resource.empty() ? "dialog-"s + dialog()->getName() : resource;
604 auto dom = domain.empty() ? dialog()->translationDomain() : domain;
605 return FGTranslate(dom).getWithDefault(res, strippedKey, strippedKey);
606}
Class for retrieving translated strings.
naRef f_translateString(const PUICompatObject &widget, nasal::CallContext ctx)
naRef propNodeGhostCreate(naContext c, SGPropertyNode *n)
naRef f_makeCompatObjectPeer(const nasal::CallContext &ctx)
naRef f_translatePluralString(const PUICompatObject &widget, nasal::CallContext ctx)
SGSharedPtr< FGPUICompatDialog > PUICompatDialogRef
SGSharedPtr< PUICompatObject > PUICompatObjectRef
std::vector< PUICompatObjectRef > PUICompatObjectVec
#define i(x)
static naRef getPropertyValue(naContext c, SGPropertyNode *node)
Convert the value of an SGPropertyNode to its Nasal representation.
naRef wrappedPropsNode(SGPropertyNode *aProps)
create Nasal props.Node for an SGPropertyNode* This is the actual ghost, wrapped in a Nasal sugar cla...
Class for retrieving translated strings.
std::string getPluralWithDefault(intType cardinalNumber, const std::string &resource, const std::string &basicId, const std::string &defaultValue, int index=0) const
Same as getWithDefault(), but for a string that has plural forms.
std::string getWithDefault(const std::string &resource, const std::string &basicId, const std::string &defaultValue, int index=0) const
Get a single translation, with default for missing or empty strings.
XML-configured GUI subsystem.
Definition new_gui.hxx:31
std::string radioGroupIdent() const
return the radio group ID associated with this widget (which is presumably a radio-button)
double width() const
bool isLive() const
bool hasBindings() const
std::string translatePluralString(const std::string &key, int cardinal, const std::string &resource={}, const std::string &domain={}) const
void valueChanged(SGPropertyNode *node) override
virtual void init()
void setEnabled(bool e)
virtual void updateGeometry(const SGRectd &newGeom)
naRef config() const
std::string translateString(const std::string &key, const std::string &resource={}, const std::string &domain={}) const
void setGeometry(const SGRectd &g)
PUICompatObject(naRef impl, const std::string &type)
double height() const
SGRectd geometry() const
const std::string & name() const
virtual void apply()
naRef show(naRef viewParent)
virtual void activateBindings()
friend naRef f_makeCompatObjectPeer(const nasal::CallContext &ctx)
PUICompatObjectRef parent() const
PUICompatDialogRef dialog() const
PUICompatObjectRef widgetByName(const std::string &name) const
find an object (which might be us, or a descendant) with the corresponding name, or nullptr.
virtual ~PUICompatObject()
static void setupGhost(nasal::Hash &guiModule)
static PUICompatObjectRef createForType(const std::string &type, SGPropertyNode_ptr config)
double getX() const
naRef propertyValue(naContext ctx) const
return the actual Nasal value of our property: this avoids the need to create a the property ghost an...
const std::string & type() const
naRef property() const
return the wrapped props,Node corresponding to our property
double getY() const
PUICompatObjectVec children() const
virtual void updateValue()
static bool isNodeAChildObject(const std::string &nm, int uiVersion)
virtual void update()
void setVisible(bool v)
const char * name
FGGlobals * globals
Definition globals.cxx:142
std::vector< std::string > string_list
Definition globals.hxx:36
SGPropertyNode * fgGetNode(const char *path, bool create)
Get a property node.
Definition proptest.cpp:27