/* Copyright (C) 2001-2006, William Joseph. All Rights Reserved. This file is part of GtkRadiant. GtkRadiant is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. GtkRadiant is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GtkRadiant; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "commands.h" #include "debugging/debugging.h" #include #include "string/string.h" #include "versionlib.h" #include "gtkutil/accelerator.h" #include "gtkutil/messagebox.h" #include "gtkmisc.h" struct ShortcutValue{ QKeySequence accelerator; const QKeySequence accelerator_default; int type; // 0 = !isRegistered, 1 = command, 2 = toggle ShortcutValue( const QKeySequence& a ) : accelerator( a ), accelerator_default( a ), type( 0 ){ } }; typedef std::map Shortcuts; Shortcuts g_shortcuts; const QKeySequence& GlobalShortcuts_insert( const char* name, const QKeySequence& accelerator ){ return ( *g_shortcuts.insert( Shortcuts::value_type( name, ShortcutValue( accelerator ) ) ).first ).second.accelerator; } template void GlobalShortcuts_foreach( Functor& functor ){ for ( auto& [name, shortcut] : g_shortcuts ) functor( name.c_str(), shortcut.accelerator ); } void GlobalShortcuts_register( const char* name, int type ){ Shortcuts::iterator i = g_shortcuts.find( name ); if ( i != g_shortcuts.end() ) { ( *i ).second.type = type; } } void GlobalShortcuts_reportUnregistered(){ for ( const auto& [name, shortcut] : g_shortcuts ) if ( !shortcut.accelerator.isEmpty() && shortcut.type == 0 ) globalWarningStream() << "shortcut not registered: " << name << "\n"; } typedef std::map Commands; Commands g_commands; void GlobalCommands_insert( const char* name, const Callback& callback, const QKeySequence& accelerator ){ bool added = g_commands.insert( Commands::value_type( name, Command( callback, GlobalShortcuts_insert( name, accelerator ) ) ) ).second; ASSERT_MESSAGE( added, "command already registered: " << makeQuoted( name ) ); } const Command& GlobalCommands_find( const char* command ){ Commands::iterator i = g_commands.find( command ); ASSERT_MESSAGE( i != g_commands.end(), "failed to lookup command " << makeQuoted( command ) ); return ( *i ).second; } typedef std::map Toggles; Toggles g_toggles; void GlobalToggles_insert( const char* name, const Callback& callback, const BoolExportCallback& exportCallback, const QKeySequence& accelerator ){ bool added = g_toggles.insert( Toggles::value_type( name, Toggle( callback, GlobalShortcuts_insert( name, accelerator ), exportCallback ) ) ).second; ASSERT_MESSAGE( added, "toggle already registered: " << makeQuoted( name ) ); } const Toggle& GlobalToggles_find( const char* name ){ Toggles::iterator i = g_toggles.find( name ); ASSERT_MESSAGE( i != g_toggles.end(), "failed to lookup toggle " << makeQuoted( name ) ); return ( *i ).second; } typedef std::map KeyEvents; KeyEvents g_keyEvents; void GlobalKeyEvents_insert( const char* name, const Callback& keyDown, const Callback& keyUp, const QKeySequence& accelerator ){ bool added = g_keyEvents.insert( KeyEvents::value_type( name, KeyEvent( GlobalShortcuts_insert( name, accelerator ), keyDown, keyUp ) ) ).second; ASSERT_MESSAGE( added, "command already registered: " << makeQuoted( name ) ); } const KeyEvent& GlobalKeyEvents_find( const char* name ){ KeyEvents::iterator i = g_keyEvents.find( name ); ASSERT_MESSAGE( i != g_keyEvents.end(), "failed to lookup keyEvent " << makeQuoted( name ) ); return ( *i ).second; } #include "mainframe.h" #include "stream/textfilestream.h" #include "stream/stringstream.h" #include #include #include #include #include #include #include #include #include #include void disconnect_accelerator( const char *name ){ Shortcuts::iterator i = g_shortcuts.find( name ); if ( i != g_shortcuts.end() ) { switch ( ( *i ).second.type ) { case 1: // command command_disconnect_accelerator( name ); break; case 2: // toggle toggle_remove_accelerator( name ); break; } } } void connect_accelerator( const char *name ){ Shortcuts::iterator i = g_shortcuts.find( name ); if ( i != g_shortcuts.end() ) { switch ( ( *i ).second.type ) { case 1: // command command_connect_accelerator( name ); break; case 2: // toggle toggle_add_accelerator( name ); break; } } } inline void accelerator_item_set_icon( QTreeWidgetItem *item, const ShortcutValue& value ){ value.accelerator != value.accelerator_default ? item->setIcon( 1, QApplication::style()->standardIcon( QStyle::StandardPixmap::SP_DialogNoButton ) ) : item->setIcon( 1, {} ); } void accelerator_clear_button_clicked( QTreeWidgetItem *item ){ const auto commandName = item->text( 0 ).toLatin1(); // clear the ACTUAL accelerator too! disconnect_accelerator( commandName ); Shortcuts::iterator thisShortcutIterator = g_shortcuts.find( commandName.constData() ); if ( thisShortcutIterator != g_shortcuts.end() ) { thisShortcutIterator->second.accelerator = {}; item->setText( 1, {} ); accelerator_item_set_icon( item, thisShortcutIterator->second ); } } // note: ideally this should also consider some shortcuts being KeyEvent and thus enabled by occasion // so technically they do not definitely clash with Command/Toggle with the same shortcut class VerifyAcceleratorNotTaken { const char *commandName; const QKeySequence newAccel; QTreeWidget *tree; public: bool allow; VerifyAcceleratorNotTaken( const char *name, const QKeySequence accelerator, QTreeWidget *tree ) : commandName( name ), newAccel( accelerator ), tree( tree ), allow( true ){ } void operator()( const char* name, QKeySequence& accelerator ){ if ( !allow || !QKeySequence_valid( accelerator ) || !strcmp( name, commandName ) ) { return; } if ( accelerator == newAccel ) { StringOutputStream msg; msg << "The command " << name << " is already assigned to the key " << accelerator << ".

" << "Do you want to unassign " << name << " first?"; const EMessageBoxReturn r = qt_MessageBox( tree->window(), msg.c_str(), "Key already used", EMessageBoxType::Question, eIDYES | eIDNO | eIDCANCEL ); if ( r == eIDYES ) { // clear the ACTUAL accelerator too! disconnect_accelerator( name ); // delete the modifier accelerator = {}; // empty the cell of the key binds dialog for( QTreeWidgetItemIterator it( tree ); *it; ++it ) { if( ( *it )->text( 0 ) == name ){ ( *it )->setText( 1, {} ); Shortcuts::const_iterator thisShortcutIterator = g_shortcuts.find( name ); if ( thisShortcutIterator != g_shortcuts.end() ) { accelerator_item_set_icon( ( *it ), thisShortcutIterator->second ); } break; } } } else if ( r == eIDCANCEL ) { // aborted allow = false; } // eIDNO : keep duplicate key } } }; // multipurpose function: invalid accelerator = reset to default static void accelerator_alter( QTreeWidgetItem *item, const QKeySequence accelerator ){ // 7. find the name of the accelerator auto commandName = item->text( 0 ).toLatin1(); Shortcuts::iterator thisShortcutIterator = g_shortcuts.find( commandName.constData() ); if ( thisShortcutIterator == g_shortcuts.end() ) { globalErrorStream() << "commandName " << makeQuoted( commandName.constData() ) << " not found in g_shortcuts.\n"; return; } // 8. build an Accelerator const QKeySequence newAccel( QKeySequence_valid( accelerator )? accelerator : thisShortcutIterator->second.accelerator_default ); // note: can skip the rest, if newAccel == current accel // 8. verify the key is still free, show a dialog to ask what to do if not VerifyAcceleratorNotTaken verify_visitor( commandName, newAccel, item->treeWidget() ); GlobalShortcuts_foreach( verify_visitor ); if ( verify_visitor.allow ) { // clear the ACTUAL accelerator first disconnect_accelerator( commandName ); thisShortcutIterator->second.accelerator = newAccel; // write into the cell item->setText( 1, newAccel.toString() ); accelerator_item_set_icon( item, thisShortcutIterator->second ); // set the ACTUAL accelerator too! connect_accelerator( commandName ); } } void accelerator_reset_all_button_clicked( QTreeWidget *tree ){ for ( const auto&[name, value] : g_shortcuts ){ // at first disconnect all to avoid conflicts during connecting if( value.accelerator != value.accelerator_default ){ // can just do this for all, but it breaks menu accelerator labels :b // clear the ACTUAL accelerator disconnect_accelerator( name.c_str() ); } } for ( auto&[name, value] : g_shortcuts ){ if( value.accelerator != value.accelerator_default ){ value.accelerator = value.accelerator_default; // set the ACTUAL accelerator connect_accelerator( name.c_str() ); } } // update tree view for( QTreeWidgetItemIterator it( tree ); *it; ++it ) { Shortcuts::const_iterator thisShortcutIterator = g_shortcuts.find( ( *it )->text( 0 ).toLatin1().constData() ); if ( thisShortcutIterator != g_shortcuts.end() ) { // write into the cell ( *it )->setText( 1, thisShortcutIterator->second.accelerator.toString() ); accelerator_item_set_icon( ( *it ), thisShortcutIterator->second ); } } } class Single_QKeySequenceEdit : public QKeySequenceEdit { protected: void keyPressEvent( QKeyEvent *e ) override { QKeySequenceEdit::keyPressEvent( e ); if( e->modifiers() & Qt::KeypadModifier ) //. workaround Qt issue: Qt::KeypadModifier is ignored setKeySequence( QKeySequence( keySequence()[0] | Qt::KeypadModifier ) ); if( QKeySequence_valid( keySequence() ) ) clearFocus(); // trigger editingFinished(); via losing focus 🙉 // because this can still receive focus loss b4 getting deleted (practically because modal msgbox) // and two editingFinished(); b no good } void focusOutEvent( QFocusEvent *event ) override { editingFinished(); } bool event( QEvent *event ) override { // comsume ALL key presses including Tab if( event->type() == QEvent::KeyPress ){ keyPressEvent( static_cast( event ) ); return true; } return QKeySequenceEdit::event( event ); } }; void accelerator_edit( QTreeWidgetItem *item ){ auto edit = new Single_QKeySequenceEdit; QObject::connect( edit, &QKeySequenceEdit::editingFinished, [item, edit](){ const QKeySequence accelerator = edit->keySequence(); item->treeWidget()->setItemWidget( item, 1, nullptr ); if( QKeySequence_valid( accelerator ) ) accelerator_alter( item, accelerator ); } ); item->treeWidget()->setItemWidget( item, 1, edit ); edit->setFocus(); // track sanity gently via edit being focused property } void DoCommandListDlg(){ QDialog dialog( MainFrame_getWindow(), Qt::Dialog | Qt::WindowCloseButtonHint ); dialog.setWindowTitle( "Mapped Commands" ); auto grid = new QGridLayout( &dialog ); auto tree = new QTreeWidget; grid->addWidget( tree, 1, 0, 1, 2 ); tree->setColumnCount( 2 ); tree->setSortingEnabled( true ); tree->sortByColumn( 0, Qt::SortOrder::AscendingOrder ); tree->setUniformRowHeights( true ); // optimization tree->setHorizontalScrollBarPolicy( Qt::ScrollBarPolicy::ScrollBarAlwaysOff ); tree->setSizeAdjustPolicy( QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents ); // scroll area will inherit column size tree->header()->setStretchLastSection( false ); // non greedy column sizing tree->header()->setSectionResizeMode( QHeaderView::ResizeMode::ResizeToContents ); // no text elision tree->setRootIsDecorated( false ); tree->setHeaderLabels( { "Command", "Key" } ); QObject::connect( tree, &QTreeWidget::itemActivated, []( QTreeWidgetItem *item, int column ){ if( item != nullptr ) accelerator_edit( item ); } ); { // Initialize dialog const auto path = StringOutputStream( 256 )( SettingsPath_get(), "commandlist.txt" ); globalOutputStream() << "Writing the command list to " << path.c_str() << "\n"; TextFileOutputStream commandList( path.c_str() ); for( const auto&[ name, value ] : g_shortcuts ) { auto item = new QTreeWidgetItem( tree, { name.c_str(), value.accelerator.toString() } ); accelerator_item_set_icon( item, value ); if ( !commandList.failed() ) { int l = strlen( name.c_str() ); commandList << name.c_str(); while ( l++ < 32 ) commandList << ' '; commandList << value.accelerator << '\n'; } } } { auto commandLine = new QLineEdit; grid->addWidget( commandLine, 0, 0 ); commandLine->setClearButtonEnabled( true ); commandLine->setPlaceholderText( QString::fromUtf8( u8"🔍 by command name" ) ); auto keyLine = new QLineEdit; grid->addWidget( keyLine, 0, 1 ); keyLine->setClearButtonEnabled( true ); keyLine->setPlaceholderText( QString::fromUtf8( u8"🔍 by keys" ) ); const auto filter = [tree]( const int column, const QString& text ){ for( QTreeWidgetItemIterator it( tree ); *it; ++it ) { ( *it )->setHidden( !( *it )->text( column ).contains( text, Qt::CaseSensitivity::CaseInsensitive ) ); } }; QObject::connect( commandLine, &QLineEdit::textChanged, [filter]( const QString& text ){ filter( 0, text ); } ); QObject::connect( keyLine, &QLineEdit::textChanged, [filter]( const QString& text ){ filter( 1, text ); } ); } { auto buttons = new QDialogButtonBox( Qt::Orientation::Vertical ); grid->addWidget( buttons, 1, 2, 1, 1 ); QPushButton *editbutton = buttons->addButton( "Edit", QDialogButtonBox::ButtonRole::ActionRole ); QObject::connect( editbutton, &QPushButton::clicked, [tree](){ if( const auto items = tree->selectedItems(); !items.isEmpty() ) accelerator_edit( items.first() ); } ); QPushButton *clearbutton = buttons->addButton( "Clear", QDialogButtonBox::ButtonRole::ActionRole ); QObject::connect( clearbutton, &QPushButton::clicked, [tree](){ if( const auto items = tree->selectedItems(); !items.isEmpty() ) accelerator_clear_button_clicked( items.first() ); } ); QPushButton *resetbutton = buttons->addButton( "Reset", QDialogButtonBox::ButtonRole::ResetRole ); QObject::connect( resetbutton, &QPushButton::clicked, [tree](){ if( const auto items = tree->selectedItems(); !items.isEmpty() ) accelerator_alter( items.first(), {} ); } ); QPushButton *resetallbutton = buttons->addButton( "Reset All", QDialogButtonBox::ButtonRole::ResetRole ); QObject::connect( resetallbutton, &QPushButton::clicked, [tree](){ accelerator_reset_all_button_clicked( tree ); } ); } dialog.exec(); } #include "profile/profile.h" const char* const COMMANDS_VERSION = "1.0-gtk-accelnames"; void SaveCommandMap( const char* path ){ StringOutputStream strINI( 256 ); strINI << path << "shortcuts.ini"; TextFileOutputStream file( strINI.c_str() ); if ( !file.failed() ) { file << "[Version]\n"; file << "number=" << COMMANDS_VERSION << "\n"; file << "\n"; file << "[Commands]\n"; auto writeCommandMap = [&file]( const char* name, const QKeySequence& accelerator ){ file << name << "="; file << accelerator; file << "\n"; }; GlobalShortcuts_foreach( writeCommandMap ); } } class ReadCommandMap { const char* m_filename; std::size_t m_count; public: ReadCommandMap( const char* filename ) : m_filename( filename ), m_count( 0 ){ } void operator()( const char* name, QKeySequence& accelerator ){ char value[1024]; if ( read_var( m_filename, "Commands", name, value ) ) { if ( string_empty( value ) ) { accelerator = {}; } else{ accelerator = QKeySequence( value ); if ( QKeySequence_valid( accelerator ) ) { ++m_count; } else { globalWarningStream() << "WARNING: failed to parse user command " << makeQuoted( name ) << ": unknown key " << makeQuoted( value ) << "\n"; } } } } std::size_t count() const { return m_count; } }; void LoadCommandMap( const char* path ){ StringOutputStream strINI( 256 ); strINI << path << "shortcuts.ini"; FILE* f = fopen( strINI.c_str(), "r" ); if ( f != 0 ) { fclose( f ); globalOutputStream() << "loading custom shortcuts list from " << makeQuoted( strINI.c_str() ) << "\n"; Version version = version_parse( COMMANDS_VERSION ); Version dataVersion = { 0, 0 }; { char value[1024]; if ( read_var( strINI.c_str(), "Version", "number", value ) ) { dataVersion = version_parse( value ); } } if ( version_compatible( version, dataVersion ) ) { globalOutputStream() << "commands import: data version " << dataVersion << " is compatible with code version " << version << "\n"; ReadCommandMap visitor( strINI.c_str() ); GlobalShortcuts_foreach( visitor ); globalOutputStream() << "parsed " << visitor.count() << " custom shortcuts\n"; } else { globalWarningStream() << "commands import: data version " << dataVersion << " is not compatible with code version " << version << "\n"; } } else { globalWarningStream() << "failed to load custom shortcuts from " << makeQuoted( strINI.c_str() ) << "\n"; } }