Radium Engine  1.5.20
Loading...
Searching...
No Matches
KeyMappingManager.cpp
1#include "KeyMappingManager.hpp"
2
3#include <Core/Resources/Resources.hpp>
4#include <Core/Utils/Log.hpp>
5
6#include <QMessageBox>
7
8namespace Ra::Gui {
9
10using namespace Core::Utils; // log
11
12KeyMappingManager::KeyMappingManager() : m_file( nullptr ) {
13
14 auto optionalPath { Core::Resources::getRadiumResourcesPath() };
15 auto resourcesRootDir { optionalPath.value_or( "[[Default resrouces path not found]]" ) };
16
18 m_defaultConfigFile = resourcesRootDir + std::string( "Configs/default.xml" );
19
20 QSettings settings;
21 QString keyMappingFilename =
22 settings.value( "keymapping/config", QString::fromStdString( m_defaultConfigFile ) )
23 .toString();
24 if ( !keyMappingFilename.contains( QString::fromStdString( m_defaultConfigFile ) ) ) {
25 LOG( logDEBUG ) << "Loading key mapping " << keyMappingFilename.toStdString() << " (from "
26 << settings.fileName().toStdString() << ")";
27 }
28 else { LOG( logDEBUG ) << "Loading default key mapping " << m_defaultConfigFile; }
29 loadConfiguration( keyMappingFilename.toStdString() );
30}
31
32KeyMappingManager::KeyMappingAction
33KeyMappingManager::getAction( const KeyMappingManager::Context& context,
34 const QEvent* event,
35 int key,
36 bool wheel ) {
37
38 Qt::MouseButtons buttons;
39 Qt::KeyboardModifiers modifiers;
40 const QMouseEvent* mouseEvent = dynamic_cast<const QMouseEvent*>( event );
41 if ( mouseEvent ) {
42 buttons = mouseEvent->buttons();
43 modifiers = mouseEvent->modifiers();
44 }
45 return getAction( context, buttons, modifiers, key, wheel );
46}
47
48KeyMappingManager::KeyMappingAction
49KeyMappingManager::getAction( const KeyMappingManager::Context& context,
50 const Qt::MouseButtons& buttons,
51 const Qt::KeyboardModifiers& modifiers,
52 int key,
53 bool wheel ) {
54
55 if ( context.isInvalid() ) {
56 LOG( logDEBUG ) << "try to get action from an invalid context";
57 return KeyMappingAction();
58 }
59 // skip key as modifiers,
60 if ( ( key == Qt::Key_Shift ) || ( key == Qt::Key_Control ) || ( key == Qt::Key_Alt ) ||
61 ( key == Qt::Key_Meta ) ) {
62 key = -1;
63 }
64
65 return getAction( context, EventBinding { buttons, modifiers, key, wheel } );
66}
67
69KeyMappingManager::getAction( const KeyMappingManager::Context& context,
70 const KeyMappingManager::EventBinding& binding ) {
71 auto action = m_bindingToAction[context].find( binding );
72 if ( action != m_bindingToAction[context].end() ) { return action->second; }
73
75}
76
77std::optional<KeyMappingManager::EventBinding>
78KeyMappingManager::getBinding( const KeyMappingManager::Context& context,
79 KeyMappingAction action ) {
80 for ( const auto& [key, value] : m_bindingToAction[context] )
81 if ( value == action ) return key;
82 return {};
83}
84
85KeyMappingManager::Context KeyMappingManager::addContext( const std::string& contextName ) {
86 Ra::Core::Utils::Index contextIndex;
87
88 auto contextItr = m_contextNameToIndex.find( contextName );
89 if ( contextItr == m_contextNameToIndex.end() ) {
90 contextIndex = m_contextNameToIndex.size();
91 m_contextNameToIndex[contextName] = contextIndex;
92 m_actionNameToIndex.emplace_back();
93 m_bindingToAction.emplace_back();
94 CORE_ASSERT( m_actionNameToIndex.size() == size_t( contextIndex + 1 ),
95 "Corrupted actionName DB" );
96 CORE_ASSERT( m_bindingToAction.size() == size_t( contextIndex + 1 ),
97 "Corrupted mappingAction DB" );
98 }
99 else
100 contextIndex = contextItr->second;
101
102 return contextIndex;
103}
104
105KeyMappingManager::Context KeyMappingManager::getContext( const std::string& contextName ) {
106 // use find so that it do not insert invalid context
107 auto itr = m_contextNameToIndex.find( contextName );
108 if ( itr != m_contextNameToIndex.end() ) return itr->second;
109 return Context {};
110}
111
112KeyMappingManager::KeyMappingAction KeyMappingManager::getAction( const Context& context,
113 const std::string& actionName ) {
114 if ( size_t( context ) >= m_actionNameToIndex.size() || context.isInvalid() ) {
115 LOG( logWARNING ) << "try to get action index ( " << actionName
116 << " ) from an invalid context ( " << context << " )";
117
118 return KeyMappingAction {};
119 }
120 auto itr = m_actionNameToIndex[context].find( actionName );
121 if ( itr != m_actionNameToIndex[context].end() ) return itr->second;
122 LOG( logWARNING ) << "try to get action index from an invalid action name " << actionName
123 << " (context " << getContextName( context ) << " [" << context << "])";
124 LOG( logWARNING ) << "consider add to conf: "
125 << "<keymap context=\"" << getContextName( context )
126 << "\" key=\"\" modifiers=\"\" buttons=\"\" action=\"" << actionName
127 << "\"/>";
128
129 return KeyMappingAction {};
130}
131
132std::string KeyMappingManager::getActionName( const Context& context,
133 const KeyMappingAction& action ) {
134
135 if ( size_t( context ) < m_actionNameToIndex.size() && context.isValid() ) {
136
137 auto actionFindItr = std::find_if(
138 std::begin( m_actionNameToIndex[context] ),
139 std::end( m_actionNameToIndex[context] ),
140 [&]( const ActionNameMap::value_type& pair ) { return pair.second == action; } );
141
142 if ( actionFindItr != std::end( m_actionNameToIndex[context] ) ) {
143 return actionFindItr->first;
144 }
145 }
146 return "Invalid";
147}
148
149std::string KeyMappingManager::getContextName( const Context& context ) {
150 auto contextFindItr = std::find_if(
151 std::begin( m_contextNameToIndex ),
152 std::end( m_contextNameToIndex ),
153 [&]( const ContextNameMap ::value_type& pair ) { return pair.second == context; } );
154
155 if ( contextFindItr != std::end( m_contextNameToIndex ) ) { return contextFindItr->first; }
156 return "Invalid";
157}
158
159int KeyMappingManager::addListener( Observer callback ) {
160 auto gid = attach( callback );
161 // call the registered listener directly to have it up to date if the
162 // config is already loaded
163 callback();
164 return gid;
165}
166
167void KeyMappingManager::removeListener( int callbackId ) {
168 detach( callbackId );
169}
170
171void KeyMappingManager::setActionBinding( const Context& contextIndex,
172 const EventBinding& binding,
173 const KeyMappingAction& actionIndex ) {
174
175 CORE_ASSERT( size_t( contextIndex ) < m_contextNameToIndex.size(),
176 "contextIndex is out of range" );
177
178 // search if an action already correspond to this binding.
179 auto f = m_bindingToAction[contextIndex].find( binding );
180 if ( f != m_bindingToAction[contextIndex].end() ) {
181
182 // if yes, search for its name
183 auto findResult = std::find_if(
184 std::begin( m_actionNameToIndex[contextIndex] ),
185 std::end( m_actionNameToIndex[contextIndex] ),
186 [&]( const ActionNameMap::value_type& pair ) { return pair.second == actionIndex; } );
187
188 // if the name is not present, something bad happens.
189 if ( findResult == std::end( m_actionNameToIndex[contextIndex] ) ) {
190 LOG( logERROR ) << "Corrupted call to bindKeyToAction, index " << actionIndex
191 << " must have been inserted before !\n";
192 return;
193 }
194
195 // if name is present, find the other action's name
196 auto findResult2 = std::find_if(
197 std::begin( m_actionNameToIndex[contextIndex] ),
198 std::end( m_actionNameToIndex[contextIndex] ),
199 [&]( const ActionNameMap::value_type& pair ) { return pair.second == f->second; } );
200
201 // if the name is not present, something bad happens.
202 if ( findResult2 == std::end( m_actionNameToIndex[contextIndex] ) ) {
203 LOG( logERROR ) << "Corrupted call to bindKeyToAction, index " << actionIndex
204 << " must have been inserted before !\n";
205 return;
206 }
207
208 LOG( logWARNING ) << "Binding action " << findResult->first << " to "
209 << "buttons [" << enumNamesFromMouseButtons( binding.m_buttons ) << "] "
210 << "modifiers [" << enumNamesFromKeyboardModifiers( binding.m_modifiers )
211 << "]"
212 << " keycode [" << binding.m_key << "]"
213 << " wheel [" << binding.m_wheel << "]"
214 << ", which is already used for action " << findResult2->first << ".";
215 }
216
217 LOG( logDEBUG2 ) << "In context " << getContextName( contextIndex ) << " [" << contextIndex
218 << "]"
219 << " binding action " << getActionName( contextIndex, actionIndex ) << " ["
220 << actionIndex << "]"
221 << " buttons [" << enumNamesFromMouseButtons( binding.m_buttons ) << "]"
222 << " modifiers [" << enumNamesFromKeyboardModifiers( binding.m_modifiers )
223 << "]"
224 << " keycode [" << binding.m_key << "]"
225 << " wheel [" << binding.m_wheel << "]";
226
227 m_bindingToAction[contextIndex][binding] = actionIndex;
228}
229
230void KeyMappingManager::loadConfiguration( const std::string& inFilename ) {
231 // if no filename is given, load default configuration
232 std::string filename = inFilename;
233 if ( filename.empty() ) { filename = m_defaultConfigFile.c_str(); }
234
235 delete m_file;
236 m_file = new QFile( QString::fromStdString( filename ) );
237
238 if ( !m_file->open( QIODevice::ReadOnly ) ) {
239 if ( filename != m_defaultConfigFile ) {
240 LOG( logERROR ) << "Failed to open key mapping configuration file ! "
241 << m_file->fileName().toStdString();
242 LOG( logERROR ) << "Trying to load default configuration...";
243 loadConfiguration();
244 return;
245 }
246 else {
247 LOG( logERROR ) << "Failed to open default key mapping configuration file !";
248 return;
249 }
250 }
251 QDomDocument domDocument;
252 if ( !domDocument.setContent( m_file ) ) {
253 LOG( logERROR ) << "Can't associate XML file to QDomDocument !";
254 LOG( logERROR ) << "Trying to load default configuration...";
255 m_file->close();
256 loadConfiguration();
257 return;
258 }
259
260 // Store setting only if not default
261 if ( filename != m_defaultConfigFile ) {
262 QSettings settings;
263 settings.setValue( "keymapping/config", m_file->fileName() );
264 }
265 m_file->close();
266
267 loadConfigurationInternal( domDocument );
268
269 // notify observer that keymapping has changed.
270 notify();
271}
272
273bool KeyMappingManager::saveConfiguration( const std::string& inFilename ) {
274 QString filename { m_file->fileName() };
275 if ( !inFilename.empty() ) { filename = QString::fromStdString( inFilename ); }
276
277 QFile saveTo( filename );
278 saveTo.open( QIODevice::WriteOnly );
279 QXmlStreamWriter stream( &saveTo );
280 stream.setAutoFormatting( true );
281 stream.setAutoFormattingIndent( 4 );
282 stream.writeStartDocument();
283 stream.writeComment( "\tRadium KeyMappingManager configuration file\t" );
284 stream.writeComment(
285 "\n<keymap context=\"theContext\" action=\"theAction\" buttons=\"QButton\" "
286 "modifier=\"QModifier\" key=\"QKey\" wheel=\"boolean\"/>\n" );
287
288 saveKeymap( stream );
289
290 stream.writeEndDocument();
291
292 if ( stream.hasError() ) {
293 LOG( logERROR ) << "Fail to write Canonical XML.";
294 return false;
295 }
296 return true;
297}
298
299void KeyMappingManager::saveKeymap( QXmlStreamWriter& stream ) {
300
301 // helper functor to write attrib
302 auto saveAttrib = [&stream]( const QString& attribName,
303 const QString& attribValue,
304 const QString& attribDefault ) {
305 if ( attribValue != attribDefault ) stream.writeAttribute( attribName, attribValue );
306 };
307
308 if ( stream.hasError() ) { return; }
309
310 stream.writeStartElement( "keymaps" );
311
312 for ( const auto& contextPair : m_contextNameToIndex ) {
313 const auto& contextName = contextPair.first;
314 const auto& context = contextPair.second;
315 for ( const auto& actionPair : m_bindingToAction[context] ) {
316 const auto& binding = actionPair.first;
317 const auto& action = actionPair.second;
318 const auto& actionNames = m_actionNameToIndex[context];
319 auto actionItr =
320 std::find_if( actionNames.begin(), actionNames.end(), [&action]( const auto& a ) {
321 return a.second == action;
322 } );
323 if ( actionItr != actionNames.end() ) {
324 const auto& actionName = actionItr->first;
325 stream.writeStartElement( "keymap" );
326 stream.writeAttribute( "context", QString::fromStdString( contextName ) );
327 stream.writeAttribute( "action", QString::fromStdString( actionName ) );
328
329 saveAttrib(
330 "buttons",
331 QString::fromStdString( enumNamesFromMouseButtons( binding.m_buttons ) ),
332 QString::fromStdString( enumNamesFromMouseButtons( Qt::NoButton ) ) );
333 saveAttrib(
334 "modifiers",
335 QString::fromStdString( enumNamesFromKeyboardModifiers( binding.m_modifiers ) ),
336 QString::fromStdString( enumNamesFromKeyboardModifiers( Qt::NoModifier ) ) );
337 saveAttrib( "key", "Key_" + QKeySequence( binding.m_key ).toString(), "Key_" );
338 saveAttrib( "wheel", binding.m_wheel ? "true" : "false", "false" );
339
340 stream.writeEndElement();
341 }
342 }
343 }
344
345 stream.writeEndElement();
346}
347
348void KeyMappingManager::loadConfigurationInternal( const QDomDocument& domDocument ) {
351 m_contextNameToIndex.clear();
352 m_actionNameToIndex.clear();
353 m_bindingToAction.clear();
354
355 QDomElement domElement = domDocument.documentElement();
356
357 if ( domElement.tagName() != "keymaps" ) {
358 LOG( logWARNING ) << "No <keymaps> global bounding tag ! Maybe you set a different global "
359 "tag ? (Not a big deal)";
360 }
361
362 QDomNode node = domElement.firstChild();
363 while ( !node.isNull() ) {
364 if ( !node.isComment() ) {
365 QDomElement nodeElement = node.toElement();
366 loadConfigurationTagsInternal( nodeElement );
367 }
368 node = node.nextSibling();
369 }
370}
371
372void KeyMappingManager::loadConfigurationTagsInternal( QDomElement& node ) {
373 if ( node.tagName() == "keymap" ) {
374
375 QDomElement e = node.toElement();
376 std::string keyString = e.attribute( "key", "-1" ).toStdString();
377 std::string modifiersString = e.attribute( "modifiers", "NoModifier" ).toStdString();
378 std::string buttonsString = e.attribute( "buttons", "NoButton" ).toStdString();
379 std::string contextName = e.attribute( "context", "AppContext" ).toStdString();
380 std::string wheelString = e.attribute( "wheel", "false" ).toStdString();
381 std::string actionName = e.attribute( "action" ).toStdString();
382
383 loadConfigurationMappingInternal(
384 contextName, keyString, modifiersString, buttonsString, wheelString, actionName );
385 }
386 else {
387 LOG( logERROR ) << "Unrecognized XML key mapping configuration file tag \""
388 << qPrintable( node.tagName() ) << "\" !";
389 LOG( logERROR ) << "Trying to load default configuration...";
390 loadConfiguration();
391
392 return;
393 }
394}
395
396KeyMappingManager::KeyMappingAction KeyMappingManager::addAction( const Context& context,
397 const std::string& actionName ) {
398 Ra::Core::Utils::Index actionIndex;
399 auto actionItr = m_actionNameToIndex[context].find( actionName );
400 if ( actionItr == m_actionNameToIndex[context].end() ) {
401 actionIndex = m_actionNameToIndex[context].size();
402 m_actionNameToIndex[context][actionName] = actionIndex;
403 }
404 else { actionIndex = actionItr->second; }
405
406 return actionIndex;
407}
408
409KeyMappingManager::KeyMappingAction KeyMappingManager::addAction( const Context& context,
410 const EventBinding& binding,
411 const std::string& actionName ) {
412 auto actionIndex = addAction( context, actionName );
413 setActionBinding( context, binding, actionIndex );
414 return actionIndex;
415}
416
417int KeyMappingManager::getKeyCode( const std::string& keyString ) {
418 auto metaEnumKey = QMetaEnum::fromType<Qt::Key>();
419 return metaEnumKey.keyToValue( keyString.c_str() );
420}
421
423KeyMappingManager::loadConfigurationMappingInternal( const std::string& context,
424 const std::string& keyString,
425 const std::string& modifiersString,
426 const std::string& buttonsString,
427 const std::string& wheelString,
428 const std::string& actionName ) {
429 auto contextIndex = addContext( context );
430 auto actionIndex = addAction( contextIndex, actionName );
431
432 auto binding =
433 createEventBindingFromStrings( buttonsString, modifiersString, keyString, wheelString );
434
435 if ( binding.m_key == -1 && binding.m_buttons == Qt::NoButton && !binding.m_wheel ) {
436 LOG( logERROR ) << "Invalid binding for action [" << actionName << "] with key ["
437 << keyString << "], buttons [" << buttonsString << "], wheel ["
438 << wheelString << "]";
439 }
440 else { setActionBinding( contextIndex, binding, actionIndex ); }
441 return actionIndex;
442}
443
444Qt::KeyboardModifiers KeyMappingManager::getQtModifiersValue( const std::string& modifierString ) {
445 Qt::KeyboardModifiers modifier = Qt::NoModifier;
446
447 std::istringstream f( modifierString );
448 std::string s;
449
450 while ( getline( f, s, ',' ) ) {
451 if ( s == "ShiftModifier" ) { modifier |= Qt::ShiftModifier; }
452 else if ( s == "ControlModifier" ) { modifier |= Qt::ControlModifier; }
453 else if ( s == "AltModifier" ) { modifier |= Qt::AltModifier; }
454 else if ( s == "MetaModifier" ) { modifier |= Qt::MetaModifier; }
455 else if ( s == "KeypadModifier" ) { modifier |= Qt::KeypadModifier; }
456 else if ( s == "GroupSwitchModifier" ) { modifier |= Qt::GroupSwitchModifier; }
457 }
458
459 return modifier;
460}
461
462Qt::MouseButtons KeyMappingManager::getQtMouseButtonsValue( const std::string& keyString ) {
463 Qt::MouseButtons key = Qt::NoButton;
464
465 if ( keyString == "LeftButton" ) { key = Qt::LeftButton; }
466 else if ( keyString == "RightButton" ) { key = Qt::RightButton; }
467 else if ( keyString == "MidButton" || keyString == "MiddleButton" ) { key = Qt::MiddleButton; }
468 else if ( keyString == "XButton1" ) { key = Qt::XButton1; }
469 else if ( keyString == "XButton2" ) { key = Qt::XButton2; }
471 return key;
472}
473
474void KeyMappingManager::reloadConfiguration() {
475 QString filename = m_file->fileName();
476 loadConfiguration( filename.toStdString().c_str() );
477}
478
479KeyMappingManager::~KeyMappingManager() {
480 if ( m_file->isOpen() ) { m_file->close(); }
481}
482
483std::string KeyMappingManager::getHelpText() {
485 for ( const auto& context : m_contextNameToIndex ) {
486 std::string contextName { context.first };
487 auto end = contextName.find( "Context" );
488 contextName = contextName.substr( 0, end );
489 text << "<h2>" << contextName << "</h2>\n";
490
491 for ( const auto& action : m_bindingToAction[context.second] ) {
492 const auto& binding = action.first;
493 const auto& actionIndex = action.second;
494 const auto& actionNames = m_actionNameToIndex[context.second];
495 const auto& actionName =
496 std::find_if( actionNames.begin(),
497 actionNames.end(),
498 [&actionIndex]( const auto& a ) { return a.second == actionIndex; } );
499 if ( actionName != actionNames.end() ) {
500 text << "<tr><td>";
501 text << "<b>" << actionName->first << "</b>";
502 text << "</td><td>";
503 if ( binding.m_buttons != Qt::NoButton )
504 text << "[" << enumNamesFromMouseButtons( binding.m_buttons ) << "] ";
505 if ( binding.m_wheel ) text << " [Wheel] ";
506 if ( binding.m_modifiers != Qt::NoModifier ) {
507
508 std::string modifierString { "Modifier" };
509 auto modifiers = enumNamesFromKeyboardModifiers( binding.m_modifiers );
510 auto found = modifiers.find( modifierString );
511 while ( found != std::string::npos ) {
512 modifiers.erase( found, modifierString.length() );
513 found = modifiers.find( modifierString, found );
514 }
515 text << "[" << modifiers << "] ";
516 }
517 if ( binding.m_key != -1 )
518 text << "[key: " << QKeySequence( binding.m_key ).toString().toStdString()
519 << "] ";
520 text << "</td></tr>\n";
521 }
522 }
523 text << "</table>\n";
524 }
525 return text.str();
526}
527
528#define TEST_BUTTON_STRING( BUTTON ) \
529 if ( buttons & Qt::BUTTON ) { \
530 returnText += sep + #BUTTON; \
531 sep = ","; \
532 }
533
534std::string KeyMappingManager::enumNamesFromMouseButtons( const Qt::MouseButtons& buttons ) {
535 std::string returnText;
536 std::string sep;
537
538 if ( buttons == Qt::NoButton ) return "NoButton";
539
540 TEST_BUTTON_STRING( LeftButton );
541 TEST_BUTTON_STRING( RightButton );
542 TEST_BUTTON_STRING( MiddleButton );
543 TEST_BUTTON_STRING( BackButton );
544 TEST_BUTTON_STRING( ForwardButton );
545 TEST_BUTTON_STRING( TaskButton );
546 TEST_BUTTON_STRING( ExtraButton4 );
547 TEST_BUTTON_STRING( ExtraButton5 );
548 TEST_BUTTON_STRING( ExtraButton6 );
549 TEST_BUTTON_STRING( ExtraButton7 );
550 TEST_BUTTON_STRING( ExtraButton8 );
551 TEST_BUTTON_STRING( ExtraButton9 );
552 TEST_BUTTON_STRING( ExtraButton10 );
553 TEST_BUTTON_STRING( ExtraButton11 );
554 TEST_BUTTON_STRING( ExtraButton12 );
555 TEST_BUTTON_STRING( ExtraButton13 );
556 TEST_BUTTON_STRING( ExtraButton14 );
557 TEST_BUTTON_STRING( ExtraButton15 );
558 TEST_BUTTON_STRING( ExtraButton16 );
559 TEST_BUTTON_STRING( ExtraButton17 );
560 TEST_BUTTON_STRING( ExtraButton18 );
561 TEST_BUTTON_STRING( ExtraButton19 );
562 TEST_BUTTON_STRING( ExtraButton20 );
563 TEST_BUTTON_STRING( ExtraButton21 );
564 TEST_BUTTON_STRING( ExtraButton22 );
565 TEST_BUTTON_STRING( ExtraButton23 );
566 TEST_BUTTON_STRING( ExtraButton24 );
567
568 return returnText;
569}
570
572KeyMappingManager::enumNamesFromKeyboardModifiers( const Qt::KeyboardModifiers& buttons ) {
573 std::string returnText;
574 std::string sep;
575
576 if ( buttons == Qt::NoModifier ) return "NoModifier";
577
578 TEST_BUTTON_STRING( ShiftModifier );
579 TEST_BUTTON_STRING( ControlModifier );
580 TEST_BUTTON_STRING( AltModifier );
581 TEST_BUTTON_STRING( MetaModifier );
582 TEST_BUTTON_STRING( KeypadModifier );
583 TEST_BUTTON_STRING( GroupSwitchModifier );
584
585 return returnText;
586}
587
588#undef TEST_BUTTON_STRING
589
591KeyMappingManager::createEventBindingFromStrings( const std::string& buttonsString,
592 const std::string& modifiersString,
593 const std::string& keyString,
594 const std::string& wheelString ) {
595 return { getQtMouseButtonsValue( buttonsString ),
596 getQtModifiersValue( modifiersString ),
597 getKeyCode( keyString ),
598 wheelString.compare( "true" ) == 0 };
599}
600
601RA_SINGLETON_IMPLEMENTATION( KeyMappingManager );
602} // namespace Ra::Gui
T begin(T... args)
T c_str(T... args)
Inner class to store event binding.
An utility class used to map a (combination of) key / modifiers / mouse buttons to a specific action....
Ra::Core::Utils::Index KeyMappingAction
handle to an action
Ra::Core::Utils::Index Context
handle to a Context
T compare(T... args)
T empty(T... args)
T end(T... args)
T find_if(T... args)
T getline(T... args)
T length(T... args)
T str(T... args)
T substr(T... args)