FlightGear next
new_gui.cxx
Go to the documentation of this file.
1/*
2 * SPDX-FileName: new_gui.cxx
3 * SPDX-FileComment: implementation of XML-configurable GUI support.
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
11
12#include <config.h>
13
14#include "new_gui.hxx"
15
16#include <cstring>
17#include <string>
18
19#include <sys/types.h>
20
21#include <simgear/compiler.h>
22#include <simgear/debug/ErrorReportingCallback.hxx>
23#include <simgear/misc/sg_dir.hxx>
24#include <simgear/props/props_io.hxx>
25#include <simgear/structure/exception.hxx>
26
28#include <Main/fg_props.hxx>
31
32#if defined(SG_MAC)
33#include "FGCocoaMenuBar.hxx"
34#endif
35
36#if defined(SG_WINDOWS)
37#include "FGWindowsMenuBar.hxx"
38#endif
39
40#include "FGNasalMenuBar.hxx"
41#include "FGPUICompatDialog.hxx"
42#include "PUICompatObject.hxx"
43
44#include "FGColor.hxx"
45
46#include "Highlight.hxx"
47
48// ignore the word Navaid here, it's a DataCache
50
51using std::string;
52
53NewGUI::DialogMetadata::DialogMetadata(const SGPath& xmlFilePath,
54 const std::string& translationDomain)
55 : xmlFilePath(xmlFilePath),
56 translationDomain(translationDomain)
57{ }
58
60// Implementation of NewGUI.
62
63
67
69{
70 for (_itt_t it = _colors.begin(); it != _colors.end(); ++it)
71 delete it->second;
72}
73
74// Recursively finds all nodes called <leaf> and appends the string values of
75// these nodes to <out>.
76//
77static void findAllLeafValues(SGPropertyNode* node, const std::string& leaf, std::vector<std::string>& out)
78{
79 string name = node->getNameString();
80 if (name == leaf) {
81 out.push_back(node->getStringValue());
82 }
83 for (int i=0; i<node->nChildren(); ++i) {
84 findAllLeafValues(node->getChild(i), leaf, out);
85 }
86}
87
88
89/* Registers menu-dialog associations with Highlight::add_menu_dialog(). */
90static void scanMenus()
91{
92 /* We find all nodes called 'dialog-name' in
93 sim/menubar/default/menu[]/item[]. */
94 SGPropertyNode* menubar = globals->get_props()->getNode("sim/menubar/default");
95 assert(menubar);
96 auto highlight = globals->get_subsystem<Highlight>();
97 if (!highlight) {
98 return;
99 }
100 for (int menu_p=0; menu_p<menubar->nChildren(); ++menu_p) {
101 SGPropertyNode* menu = menubar->getChild(menu_p);
102 if (menu->getNameString() != "menu") continue;
103 for (int item_p=0; item_p<menu->nChildren(); ++item_p) {
104 SGPropertyNode* item = menu->getChild(item_p);
105 if (item->getNameString() != "item") continue;
106 std::vector<std::string> dialog_names;
107 findAllLeafValues(item, "dialog-name", dialog_names);
108 for (auto dialog_name: dialog_names) {
109 highlight->addMenuDialog(HighlightMenu(menu->getIndex(), item->getIndex()), dialog_name);
110 }
111 }
112 }
113}
114
115void
117{
118 createMenuBarImplementation();
119 fgTie("/sim/menubar/visibility", this,
121
122 fgTie("/sim/menubar/overlap-hide", this,
124
125 setStyle();
126 SGPath p(globals->get_fg_root(), "gui/dialogs");
127 readDir(p, "core");
128
129 if (fgGetBool("/sim/gui/startup") == false) {
130 SGPath aircraftDialogDir(fgGetString("/sim/aircraft-dir"), "gui/dialogs");
131 if (aircraftDialogDir.exists()) {
132 readDir(aircraftDialogDir, "current-aircraft");
133 }
134
135 // Read XML dialogs made available by registered add-ons
136 const auto& addonManager = flightgear::addons::AddonManager::instance();
137 if (addonManager) {
138 for (const auto& addon : addonManager->registeredAddons()) {
139 SGPath addonDialogDir = addon->getBasePath() / "gui/dialogs";
140
141 if (addonDialogDir.exists()) {
142 readDir(addonDialogDir, "addons/" + addon->getId());
143 }
144 }
145 }
146 }
147
148 if (_menubar) {
149 // Fix for http://code.google.com/p/flightgear-bugs/issues/detail?id=947
150 fgGetNode("sim/menubar")->setAttribute(SGPropertyNode::PRESERVE, true);
151 _menubar->init();
152 scanMenus();
153 }
154}
155
156void
158{
159 _active_dialogs.clear();
160 _active_dialog.clear();
161
162 fgUntie("/sim/menubar/visibility");
163 fgUntie("/sim/menubar/overlap-hide");
164 _menubar.reset();
165 _dialog_props.clear();
166}
167
168void
170{
171 reset(true);
172 fgSetBool("/sim/signals/reinit-gui", true);
173}
174
175void
177{
178 reset(false);
179}
180
181void
182NewGUI::createMenuBarImplementation()
183{
184 if (!fgGetBool("/sim/menubar/enable", true)) {
185 SG_LOG(SG_GUI, SG_INFO, "Menubar is disabled");
186 return;
187 }
188
189#if defined(SG_MAC)
190 if (fgGetBool("/sim/menubar/native", true)) {
191 _menubar.reset(new FGCocoaMenuBar);
192 }
193#endif
194#if defined(SG_WINDOWS)
195 if (fgGetBool("/sim/menubar/native", false)) {
196 _menubar.reset(new FGWindowsMenuBar);
197 }
198#endif
199 if (!_menubar.get()) {
200 _menubar.reset(new FGNasalMenuBar);
201 }
202}
203
204void NewGUI::setDialogMetadata(const string& name, const SGPath& xmlFilepath,
205 const string& domain)
206{
207 _dialog_metadata.erase(name);
208 _dialog_metadata.emplace(name, DialogMetadata(xmlFilepath, domain));
209}
210
211void
212NewGUI::reset (bool reload)
213{
214 DialogDict::iterator iter;
215 string_list openDialogs;
216 // close all open dialogs and remember them ...
217 for (iter = _active_dialogs.begin(); iter != _active_dialogs.end(); ++iter)
218 openDialogs.push_back(iter->first);
219
220 for (auto d : openDialogs)
221 closeDialog(d);
222
223 setStyle();
224
225 unbind();
226
227 if (reload) {
228 _dialog_props.clear();
229 _dialog_metadata.clear();
230 init();
231 } else {
232 createMenuBarImplementation();
233 if (_menubar) {
234 _menubar->init();
235 }
236 }
237
238 bind();
239
240 // open dialogs again
241 for (auto d : openDialogs)
242 showDialog(d);
243}
244
245void
247{
248}
249
250void
252{
253}
254
256{
257 auto nas = globals->get_subsystem<FGNasalSys>();
258 nasal::Context ctx;
259 nasal::Hash guiModule{nas->getModule("gui"), ctx};
260 nasal::Hash compatModule = guiModule.createHash("xml");
261
262 FGPUICompatDialog::setupGhost(compatModule);
263 PUICompatObject::setupGhost(compatModule);
264 FGNasalMenuBar::setupGhosts(compatModule);
265
266 if (_menubar) {
267 _menubar->postinit();
268 }
269}
270
271void
272NewGUI::update (double delta_time_sec)
273{
274 SG_UNUSED(delta_time_sec);
275 auto iter = _active_dialogs.begin();
276 for(/**/; iter != _active_dialogs.end(); iter++)
277 iter->second->update();
278}
279
280bool
282{
283 if (name.empty()) {
284 SG_LOG(SG_GENERAL, SG_ALERT, "showDialog: no dialog name provided");
285 return false;
286 }
287
288 // first, check if it's already shown
289 if (_active_dialogs.find(name) != _active_dialogs.end()){
290 _active_dialogs[name]->bringToFront();
291 return true;
292 }
293
294 // Check we know about the dialog by name
295 const auto metadataIt = _dialog_metadata.find(name);
296 if (metadataIt == _dialog_metadata.end()) {
297 simgear::reportFailure(simgear::LoadFailure::NotFound,
298 simgear::ErrorCode::GUIDialog,
299 "Metadata not found for dialog '" + name + "'");
300 return false;
301 }
302
303 flightgear::addSentryBreadcrumb("showing GUI dialog:" + name, "info");
304 try {
305 SGSharedPtr<FGPUICompatDialog> pcd = new FGPUICompatDialog(
306 getDialogProperties(name), metadataIt->second.translationDomain);
307 if (pcd->init()) {
308 _active_dialogs[name] = pcd; // establish ownership
309 } else {
310 return false;
311 }
312
313 fgSetString("/sim/gui/dialogs/current-dialog", name);
314
315 // setActiveDialog(new FGPUIDialog(getDialogProperties(name)));
316 return true;
317 } catch (sg_exception& e) {
318 simgear::reportFailure(simgear::LoadFailure::Misconfigured, simgear::ErrorCode::GUIDialog, "Dialog failed to show:" + name + ":" + e.getFormattedMessage(), e.getLocation());
319 return false;
320 }
321}
322
323bool
325{
326 if (_active_dialogs.find(name) == _active_dialogs.end()) {
328 return true;
329 }
330 else {
332 return false;
333 }
334}
335
336bool
338{
339 if (_active_dialog == 0)
340 return false;
341
342 // TODO support a request-close callback here, optionally
343 // many places in code assume this code-path does an
344 // immediate close, but for some UI paths it would be nice to
345 // allow some dialogs the chance to inervene
346
347 // Kill any entries in _active_dialogs... Is there an STL
348 // algorithm to do (delete map entries by value, not key)? I hate
349 // the STL :) -Andy
350 auto iter = _active_dialogs.begin();
351 for(/**/; iter != _active_dialogs.end(); iter++) {
352 if(iter->second == _active_dialog) {
353 _active_dialog->close();
354
355 _active_dialogs.erase(iter);
356 // iter is no longer valid
357 break;
358 }
359 }
360
361 _active_dialog = 0;
362 if (!_active_dialogs.empty()) {
363 fgSetString("/sim/gui/dialogs/current-dialog", _active_dialogs.begin()->second->getName());
364 }
365 return true;
366}
367
368bool
370{
371 if(_active_dialogs.find(name) != _active_dialogs.end()) {
372 flightgear::addSentryBreadcrumb("closing GUI dialog:" + name, "info");
373
374 if(_active_dialog == _active_dialogs[name])
375 _active_dialog.clear();
376
377 _active_dialogs.erase(name);
378 return true;
379 }
380 return false; // dialog wasn't open...
381}
382
383SGPropertyNode_ptr
385{
386 const auto metadataIt = _dialog_metadata.find(name);
387
388 if (metadataIt == _dialog_metadata.end()) {
389 SG_LOG(SG_GENERAL, SG_ALERT, "Dialog '" << name << "' not defined");
390 return {};
391 }
392
393 NameDialogDict::iterator it = _dialog_props.find(name);
394 if (it == _dialog_props.end()) {
395 // load the XML
396 const SGPath path = metadataIt->second.xmlFilePath;
397 SGPropertyNode_ptr props = new SGPropertyNode;
398
399 try {
400 readProperties(path, props);
401 } catch (const sg_exception &) {
402 SG_LOG(SG_INPUT, SG_ALERT, "Error parsing dialog from " << path);
403 return {};
404 }
405
406 it = _dialog_props.insert(it, std::make_pair(name, props));
407 }
408
409 return it->second;
410}
411
414{
415 if(_active_dialogs.find(name) != _active_dialogs.end())
416 return _active_dialogs[name];
417
418 SG_LOG(SG_GENERAL, SG_DEBUG, "dialog '" << name << "' missing");
419 return 0;
420}
421
422void
424{
425 if (dialog){
426 fgSetString("/sim/gui/dialogs/current-dialog", dialog->getName());
427 }
428 _active_dialog = dialog;
429}
430
433{
434 return _active_dialog;
435}
436
437FGMenuBar *
439{
440 return _menubar.get();
441}
442
443bool
445{
446 if (_menubar) {
447 return _menubar->isVisible();
448 }
449
450 return false;
451}
452
453void
455{
456 if (_menubar) {
457 if (visible)
458 _menubar->show();
459 else
460 _menubar->hide();
461 }
462}
463
464bool
466{
467 return _menubar && _menubar->getHideIfOverlapsWindow();
468}
469
470void
472{
473 if (_menubar) {
474 _menubar->setHideIfOverlapsWindow(hide);
475 }
476}
477
478void
479NewGUI::newDialog (SGPropertyNode* props)
480{
481 string name = props->getStringValue("name", "");
482 if(name.empty()) {
483 SG_LOG(SG_GENERAL, SG_ALERT, "New dialog has no <name> property");
484 return;
485 }
486
487 if(_active_dialogs.find(name) == _active_dialogs.end()) {
488 _dialog_props[name] = props;
489 // Use a dummy XML file path, so we believe the dialog exists. The
490 // translation domain can be overridden from the 'props' argument.
491 setDialogMetadata(name, SGPath());
492 }
493}
494
495
496void
497NewGUI::readDir (const SGPath& path, const std::string& translationDomain)
498{
499 simgear::Dir dir(path);
500 if( !dir.exists() )
501 {
502 SG_LOG(SG_INPUT, SG_INFO, "directory does not exist: " << path);
503 return;
504 }
505
506 flightgear::NavDataCache* cache = flightgear::NavDataCache::instance();
508 auto highlight = globals->get_subsystem<Highlight>();
509 for (SGPath xmlPath : dir.children(simgear::Dir::TYPE_FILE, ".xml")) {
510
511 SGPropertyNode_ptr props = new SGPropertyNode;
512 SGPropertyNode *nameprop = nullptr;
513 std::string name;
514
515 /* Always parse the dialog even if cache->isCachedFileModified() says
516 it is cached, so that we can register property-dialog associations with
517 Highlight::add_property_dialog(). */
518 try {
519 readProperties(xmlPath, props);
520 } catch (const sg_exception &) {
521 SG_LOG(SG_INPUT, SG_ALERT, "Error parsing dialog " << xmlPath);
522 props.reset();
523 }
524 if (props) {
525 nameprop = props->getNode("name");
526 if (nameprop) {
527 name = nameprop->getStringValue();
528 // Look for 'property' nodes within the dialog. (could
529 // also look for "dialog-name" to catch things like
530 // dialog-show.)
531 std::vector<std::string> property_paths;
532 findAllLeafValues(props, "property", property_paths);
533 for (auto property_path: property_paths) {
534 // We could maybe hoist this test for hightlight to avoid reaching
535 // here if it is null, but that's difficult to test, so we do it
536 // here instead.
537 if (highlight) {
538 highlight->addPropertyDialog(property_path, name);
539 }
540 }
541 }
542 }
543
544 if (!cache->isCachedFileModified(xmlPath)) {
545 // cached, easy
546 string name = cache->readStringProperty(xmlPath.utf8Str());
547 // The translation domain can be overridden from the xmlPath contents
548 setDialogMetadata(name, xmlPath, translationDomain);
549 continue;
550 }
551
552 // we need to parse the actual XML
553 if (!props) {
554 SG_LOG(SG_INPUT, SG_ALERT, "Error parsing dialog " << xmlPath);
555 continue;
556 }
557
558 if (!nameprop) {
559 SG_LOG(SG_INPUT, SG_WARN, "dialog " << xmlPath << " has no name; skipping.");
560 continue;
561 }
562
563 setDialogMetadata(name, xmlPath, translationDomain);
564 // update cached values
565 if (!cache->isReadOnly()) {
566 cache->stampCacheFile(xmlPath);
567 cache->writeStringProperty(xmlPath.utf8Str(), name);
568 }
569 } // of directory children iteration
570
571 txn.commit();
572}
573
575// Style handling.
577
578void
580{
581 _itt_t it;
582 for (it = _colors.begin(); it != _colors.end(); ++it)
583 delete it->second;
584 _colors.clear();
585
586 // set up the traditional colors as default
587 _colors["background"] = new FGColor(0.8f, 0.8f, 0.9f, 0.85f);
588 _colors["foreground"] = new FGColor(0.0f, 0.0f, 0.0f, 1.0f);
589 _colors["highlight"] = new FGColor(0.7f, 0.7f, 0.7f, 1.0f);
590 _colors["label"] = new FGColor(0.0f, 0.0f, 0.0f, 1.0f);
591 _colors["legend"] = new FGColor(0.0f, 0.0f, 0.0f, 1.0f);
592 _colors["misc"] = new FGColor(0.0f, 0.0f, 0.0f, 1.0f);
593 _colors["inputfield"] = new FGColor(0.8f, 0.7f, 0.7f, 1.0f);
594
595 //puSetDefaultStyle();
596
597
598 if (0) {
599 // Re-read gui/style/*.xml files so that one can edit them and see the
600 // results without restarting flightgear.
601 SGPath p(globals->get_fg_root(), "gui/styles");
602 SGPropertyNode* sim_gui = globals->get_props()->getNode("sim/gui/", true);
603 simgear::Dir dir(p);
604 int i = 0;
605 for (SGPath xml: dir.children(simgear::Dir::TYPE_FILE, ".xml")) {
606 SGPropertyNode_ptr node = sim_gui->getChild("style", i, true);
607 node->removeAllChildren();
608 SG_LOG(SG_GENERAL, SG_WARN, "reading from " << xml << " into " << node->getPath());
609 readProperties(xml, node);
610 i += 1;
611 }
612 }
613
614 int which = fgGetInt("/sim/gui/current-style", 0);
615 SGPropertyNode *sim = globals->get_props()->getNode("sim/gui", true);
616 SGPropertyNode *n = sim->getChild("style", which);
617 if (!n)
618 n = sim->getChild("style", 0, true);
619
620 SGPropertyNode *selected_style = globals->get_props()->getNode("sim/gui/selected-style", true);
621
622 // n->copy() doesn't delete existing nodes, so need to clear them all
623 // first.
624 selected_style->removeAllChildren();
625 copyProperties(n, selected_style);
626
627 //if (selected_style && n)
628 // n->alias(selected_style);
629
630 //setupFont(n->getNode("fonts/gui", true));
631 n = n->getNode("colors", true);
632
633 for (int i = 0; i < n->nChildren(); i++) {
634 SGPropertyNode *child = n->getChild(i);
635 _colors[child->getNameString()] = new FGColor(child);
636 }
637}
638
639
640// Register the subsystem.
641SGSubsystemMgr::Registrant<NewGUI> registrantNewGUI(
642 SGSubsystemMgr::INIT);
643
644// end of new_gui.cxx
#define p(x)
#define i(x)
An XML-configured dialog box.
Definition dialog.hxx:21
virtual const char * getName()
Definition dialog.hxx:63
XML-configured menu bar interface.
Definition menubar.hxx:19
virtual bool getHideIfOverlapsWindow() const =0
static void setupGhosts(nasal::Hash &compatModule)
An XML-configured dialog box.
static void setupGhost(nasal::Hash &compatModule)
void shutdown() override
Definition new_gui.cxx:157
void unbind() override
Definition new_gui.cxx:251
virtual SGPropertyNode_ptr getDialogProperties(const std::string &name)
Get dialog property tree's root node.
Definition new_gui.cxx:384
virtual bool closeDialog(const std::string &name)
Close a named dialog, if it is open.
Definition new_gui.cxx:369
SGSharedPtr< FGDialog > FGDialogRef
Definition new_gui.hxx:33
void init() override
Definition new_gui.cxx:116
virtual void setMenuBarVisible(bool visible)
Show or hide the menubar.
Definition new_gui.cxx:454
void setMenuBarOverlapHide(bool hide)
Definition new_gui.cxx:471
virtual FGDialogRef getActiveDialog()
Get the dialog currently active, if any.
Definition new_gui.cxx:432
virtual void reset(bool reload)
Used by reinit() and redraw() to close all dialogs and to apply current GUI colors.
Definition new_gui.cxx:212
void bind() override
Definition new_gui.cxx:246
void update(double delta_time_sec) override
Definition new_gui.cxx:272
void reinit() override
Definition new_gui.cxx:169
virtual bool showDialog(const std::string &name)
Display a dialog box.
Definition new_gui.cxx:281
void postinit() override
Definition new_gui.cxx:255
virtual bool toggleDialog(const std::string &name)
Toggle display of a dialog box.
Definition new_gui.cxx:324
virtual void redraw()
Redraw the GUI picking up new GUI colors.
Definition new_gui.cxx:176
virtual FGMenuBar * getMenuBar()
Return a pointer to the current menubar.
Definition new_gui.cxx:438
virtual void setStyle()
Definition new_gui.cxx:579
virtual bool getMenuBarVisible() const
Test if the menubar is visible.
Definition new_gui.cxx:444
virtual ~NewGUI()
Destructor.
Definition new_gui.cxx:68
virtual void setActiveDialog(FGDialog *dialog)
Ignore this method.
Definition new_gui.cxx:423
virtual bool closeActiveDialog()
Close the currenty active dialog.
Definition new_gui.cxx:337
bool getMenuBarOverlapHide() const
Definition new_gui.cxx:465
virtual void newDialog(SGPropertyNode *node)
Creates a new dialog box, using the same property format as the gui/dialogs configuration files.
Definition new_gui.cxx:479
virtual FGDialogRef getDialog(const std::string &name)
Get the named dialog if active.
Definition new_gui.cxx:413
NewGUI()
Constructor.
Definition new_gui.cxx:64
static void setupGhost(nasal::Hash &guiModule)
void stampCacheFile(const SGPath &path, const std::string &sha={})
void writeStringProperty(const std::string &key, const std::string &value)
std::string readStringProperty(const std::string &key)
static NavDataCache * instance()
bool isCachedFileModified(const SGPath &path) const
static const std::unique_ptr< AddonManager > & instance()
const char * name
void fgUntie(const char *name)
Untie a property from an external data source.
Definition fg_props.cxx:634
int fgGetInt(const char *name, int defaultValue)
Get an int value for a property.
Definition fg_props.cxx:532
std::string fgGetString(const char *name, const char *defaultValue)
Get a string value for a property.
Definition fg_props.cxx:556
void fgTie(const char *name, V(*getter)(), void(*setter)(V)=0, bool useDefault=true)
Tie a property to a pair of simple functions.
Definition fg_props.hxx:751
FGGlobals * globals
Definition globals.cxx:142
std::vector< std::string > string_list
Definition globals.hxx:36
void addSentryBreadcrumb(const std::string &, const std::string &)
static void scanMenus()
Definition new_gui.cxx:90
static void findAllLeafValues(SGPropertyNode *node, const std::string &leaf, std::vector< std::string > &out)
Definition new_gui.cxx:77
SGSubsystemMgr::Registrant< NewGUI > registrantNewGUI(SGSubsystemMgr::INIT)
bool fgGetBool(char const *name, bool def)
Get a bool value for a property.
Definition proptest.cpp:25
bool fgSetBool(char const *name, bool val)
Set a bool value for a property.
Definition proptest.cpp:24
bool fgSetString(char const *name, char const *str)
Set a string value for a property.
Definition proptest.cpp:26
SGPropertyNode * fgGetNode(const char *path, bool create)
Get a property node.
Definition proptest.cpp:27