FlightGear next
FGNasalMenuBar.cxx
Go to the documentation of this file.
1/*
2 * SPDX-FileName: NasalMenuBar.hxx
3 * SPDX-FileComment: XML-configured menu bar
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 * SPDX-FileCopyrightText: Copyright (C) 2023 James Turner
6 */
7
9
10// std
11#include <vector>
12
13// SimGear
14#include <simgear/misc/strutils.hxx>
15#include <simgear/nasal/cppbind/Ghost.hxx>
16#include <simgear/props/props.hxx>
17#include <simgear/structure/SGBinding.hxx>
18
19#include <Main/fg_props.hxx>
20#include <Main/globals.hxx>
22
23static bool nameIsSeparator(const std::string& n)
24{
25 return simgear::strutils::starts_with(simgear::strutils::strip(n), "----");
26}
27
28class NasalMenu;
29class NasalMenuItem;
30
31using NasalMenuPtr = SGSharedPtr<NasalMenu>;
32using NasalMenuItemPtr = SGSharedPtr<NasalMenuItem>;
33
40
42
43class NasalMenuItem : public SGReferenced,
44 public SGPropertyChangeListener
45{
46public:
47 std::string name() const
48 {
49 return _name;
50 }
51
52 std::string shortcut() const
53 {
54 return _shortcut;
55 }
56
57 bool isEnabled() const
58 {
59 return _enabled;
60 }
61
62 bool isChecked() const
63 {
64 return _checked;
65 }
66
67 bool isSeparator() const
68 {
69 return _isSeparator;
70 }
71
72 bool isCheckable() const
73 {
74 return _isCheckable;
75 }
76
77 std::string label() const
78 {
79 return _label;
80 }
81
82 void fire();
83
84 void aboutToShow();
85
87 {
88 return _submenu;
89 }
90
91 void initFromNode(SGPropertyNode_ptr config);
92
93 using NasalCallback = std::function<void()>;
94
96 {
97 _callbacks.push_back(cb);
98 }
99
100protected:
101 void valueChanged(SGPropertyNode* prop) override;
102
103private:
104 void runCallbacks()
105 {
106 std::for_each(_callbacks.begin(), _callbacks.end(), [](NasalCallback& cb) {
107 cb();
108 });
109 }
110
111 std::string _name;
112 std::string _label;
113 std::string _shortcut;
114 bool _isSeparator = false;
115 bool _isCheckable = false;
116 bool _enabled = true;
117 bool _checked = false;
118
119 SGPropertyNode_ptr _enabledNode,
120 _checkedNode, _labelNode;
121 NasalMenuPtr _submenu;
122 SGBindingList _bindings;
123 std::vector<NasalCallback> _callbacks;
124};
125
126class NasalMenu : public SGReferenced,
127 public SGPropertyChangeListener
128{
129public:
130 using ItemsVec = std::vector<NasalMenuItemPtr>;
131
132 std::string label() const
133 {
134 return _label;
135 }
136
137 std::string name() const
138 {
139 return _name;
140 }
141
142 bool isEnabled() const
143 {
144 return _enabled;
145 }
146
147 const ItemsVec& items() const
148 {
149 return _items;
150 }
151
152 void aboutToShow();
153
154 void initFromNode(SGPropertyNode_ptr config);
155
156protected:
157 void valueChanged(SGPropertyNode* prop) override;
158
159private:
160 std::string _name;
161 std::string _label;
162 bool _enabled = true;
163 SGPropertyNode_ptr _enabledNode, _labelNode;
164 ItemsVec _items;
165};
166
167
169
170void NasalMenuItem::initFromNode(SGPropertyNode_ptr config)
171{
172 auto n = config->getChild("name");
173 if (!n) {
174 SG_LOG(SG_GUI, SG_DEV_WARN, "menu item without <name> element:" << config->getLocation());
175 } else {
176 _name = n->getStringValue();
177 n->addChangeListener(this);
178
179 if (n->getBoolValue("separator") || nameIsSeparator(_name)) {
180 _isSeparator = true;
181 }
182 }
183
184 auto _labelNode = config->getChild("label");
185 if (_labelNode) {
186 _labelNode->addChangeListener(this);
187 }
188 _label = FGMenuBar::getLocalizedLabel(config);
189
190 _checkedNode = config->getChild("checked");
191 if (_checkedNode) {
192 _isCheckable = true;
193 _checked = _checkedNode->getBoolValue();
194 _checkedNode->addChangeListener(this);
195 }
196 // todo support checked-condition
197
198 // we always create an enabled node, so we can decide later
199 // to dynamically disable the menu. Otherwise our listener
200 // wouldn't work
201 _enabledNode = config->getChild("enabled");
202 if (_enabledNode) {
203 _enabled = _enabledNode->getBoolValue();
204 } else {
205 _enabledNode = config->addChild("enabled");
206 _enabledNode->setBoolValue(true); // default to enabled
207 }
208 _enabledNode->addChangeListener(this);
209
210 // TODO support enabled-condition
211
212 n = config->getChild("key");
213 if (n) {
214 _shortcut = n->getStringValue();
215 }
216
217 auto bindingNodes = config->getChildren("binding");
218 _bindings = readBindingList(bindingNodes, globals->get_props());
219
220 n = config->getChild("menu");
221 if (n) {
222 _submenu = new NasalMenu();
223 _submenu->initFromNode(n);
224 }
225}
226
227void NasalMenuItem::valueChanged(SGPropertyNode* n)
228{
229 // sort this by likelyhood these changing, to avoid
230 // unnecessary string comaprisons
231 if (n == _enabledNode) {
232 _enabled = _enabledNode->getBoolValue();
233 } else if (n == _checkedNode) {
234 _checked = _checkedNode->getBoolValue();
235 } else if (n == _labelNode) {
236 _label = FGMenuBar::getLocalizedLabel(_labelNode->getParent());
237 } else if (n->getNameString() == "name") {
238 _name = n->getStringValue();
239 }
240
241 // allow Nasal to response to changes
242 runCallbacks();
243}
244
246{
247 if (!_enabled) {
248 return;
249 }
250
251 SGPropertyNode_ptr args{new SGPropertyNode};
252 args->setBoolValue("checked", _checked);
253 fireBindingList(_bindings, args);
254}
255
257{
258 if (_submenu) {
259 // update submenu title / enabled-ness
260 }
261}
262
264
265
266void NasalMenu::initFromNode(SGPropertyNode_ptr config)
267{
268 const auto name = config->getStringValue("name");
269 _name = name;
270
271 _enabledNode = config->getChild("enabled");
272 if (_enabledNode) {
273 _enabled = _enabledNode->getBoolValue();
274 } else {
275 _enabledNode = config->addChild("enabled");
276 _enabledNode->setBoolValue(true); // default to enabled
277 }
278 _enabledNode->addChangeListener(this);
279
280 auto _labelNode = config->getChild("label");
281 if (_labelNode) {
282 _labelNode->addChangeListener(this);
283 }
284 _label = FGMenuBar::getLocalizedLabel(config);
285
286 for (auto i : config->getChildren("item")) {
287 auto it = new NasalMenuItem;
288 it->initFromNode(i);
289 _items.push_back(it);
290 }
291}
292
294{
295 for (auto it : _items) {
296 it->aboutToShow();
297 }
298}
299
300void NasalMenu::valueChanged(SGPropertyNode* n)
301{
302 if (n == _enabledNode) {
303 _enabled = n->getBoolValue();
304 } else if (n == _labelNode) {
305 _label = FGMenuBar::getLocalizedLabel(n->getParent());
306 }
307}
308
310
312{
313public:
314 std::vector<NasalMenuPtr> getMenus() const
315 {
316 return menus;
317 }
318
321 std::vector<NasalMenuPtr> menus;
322};
323
325
329
331{
332 SGPropertyNode_ptr props = fgGetNode("/sim/menubar/default", true);
333 configure(props);
334}
335
337{
338 auto nas = globals->get_subsystem<FGNasalSys>();
339 nasal::Context ctx;
340 nasal::Hash guiModule{nas->getModule("gui"), ctx};
341
342 using MenuBarRef = std::shared_ptr<NasalMenuBarPrivate>;
343 auto f = guiModule.get<std::function<void(MenuBarRef)>>("_createMenuBar");
344 if (!f) {
345 SG_LOG(SG_GUI, SG_DEV_ALERT, "GUI: _createMenuBar implementation not found");
346 return;
347 }
348
349 // invoke nasal callback to build up the menubar
350 f(_d);
351}
352
354{
355 _d->visibilityMode = VisibilityMode::Visible;
356 recomputeVisibility();
357}
358
360{
361 _d->visibilityMode = VisibilityMode::Hidden;
362 recomputeVisibility();
363}
364
365void FGNasalMenuBar::configure(SGPropertyNode_ptr config)
366{
367 _d->menus.clear();
368 for (auto i : config->getChildren("menu")) {
369 auto m = new NasalMenu;
370 m->initFromNode(i);
371 _d->menus.push_back(m);
372 }
373}
374
375void FGNasalMenuBar::recomputeVisibility()
376{
377}
378
380{
381 return _d->computedVisibility;
382}
383
385{
386 _d->visibilityMode = VisibilityMode::HideIfOverlapsWindow;
387 recomputeVisibility();
388}
389
391{
392 return _d->visibilityMode == VisibilityMode::HideIfOverlapsWindow;
393}
394
395static naRef f_itemAddCallback(NasalMenuItem& item, const nasal::CallContext& ctx)
396{
397 auto cb = ctx.requireArg<NasalMenuItem::NasalCallback>(0);
398 item.addCallback(cb);
399 return naNil();
400}
401
402void FGNasalMenuBar::setupGhosts(nasal::Hash& compatModule)
403{
404 using MenuItemGhost = nasal::Ghost<NasalMenuItemPtr>;
405 MenuItemGhost::init("gui.xml.MenuItem")
406 .member("name", &NasalMenuItem::name)
407 .member("enabled", &NasalMenuItem::isEnabled)
408 .member("checked", &NasalMenuItem::isChecked)
409 .member("checkable", &NasalMenuItem::isCheckable)
410 .member("separator", &NasalMenuItem::isSeparator)
411 .member("shortcut", &NasalMenuItem::shortcut)
412 .member("submenu", &NasalMenuItem::submenu)
413 .member("label", &NasalMenuItem::label)
414 .method("fire", &NasalMenuItem::fire)
415 .method("addChangedCallback", &f_itemAddCallback);
416
417 using MenuGhost = nasal::Ghost<NasalMenuPtr>;
418 MenuGhost::init("gui.xml.Menu")
419 .member("label", &NasalMenu::label)
420 .member("name", &NasalMenu::name)
421 .member("enabled", &NasalMenu::isEnabled)
422 .member("items", &NasalMenu::items);
423
424
425 using MenuBarGhost = nasal::Ghost<std::shared_ptr<NasalMenuBarPrivate>>;
426 MenuBarGhost::init("gui.xml.MenuBar")
427 .member("menus", &NasalMenuBarPrivate::getMenus);
428}
SGSharedPtr< NasalMenu > NasalMenuPtr
VisibilityMode
static naRef f_itemAddCallback(NasalMenuItem &item, const nasal::CallContext &ctx)
static bool nameIsSeparator(const std::string &n)
SGSharedPtr< NasalMenuItem > NasalMenuItemPtr
#define i(x)
static std::string getLocalizedLabel(SGPropertyNode *node)
Read a menu label from the menu's property tree.
Definition menubar.cxx:19
std::vector< NasalMenuPtr > menus
std::vector< NasalMenuPtr > getMenus() const
void hide() override
Make the menu bar invisible.
void show() override
Make the menu bar visible.
FGNasalMenuBar()
Constructor.
void setHideIfOverlapsWindow(bool hide) override
Request the menubar to be hidden if its display overlays the main window content.
static void setupGhosts(nasal::Hash &compatModule)
bool isVisible() const override
Test whether the menu bar is visible.
void init() override
Initialize the menu bar from $FG_ROOT/gui/menubar.xml.
bool getHideIfOverlapsWindow() const override
void postinit() override
std::function< void()> NasalCallback
std::string name() const
std::string label() const
bool isCheckable() const
std::string shortcut() const
void valueChanged(SGPropertyNode *prop) override
bool isEnabled() const
bool isChecked() const
bool isSeparator() const
void initFromNode(SGPropertyNode_ptr config)
NasalMenuPtr submenu() const
void addCallback(NasalCallback cb)
const ItemsVec & items() const
bool isEnabled() const
void valueChanged(SGPropertyNode *prop) override
std::string name() const
std::string label() const
void initFromNode(SGPropertyNode_ptr config)
std::vector< NasalMenuItemPtr > ItemsVec
FGGlobals * globals
Definition globals.cxx:142
SGPropertyNode * fgGetNode(const char *path, bool create)
Get a property node.
Definition proptest.cpp:27