netradiant-custom/radiant/commands.cpp
Garux df02774ff5 tweak StringOutputStream use
auto str = StringOutputStream()(bla) use form was not doing copy elision or move, but copy
2024-01-29 16:54:08 +06:00

518 lines
17 KiB
C++

/*
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 <map>
#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<CopiedString, ShortcutValue> 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<typename Functor>
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<CopiedString, Command> 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<CopiedString, Toggle> 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<CopiedString, KeyEvent> 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 <QDialog>
#include <QTreeWidget>
#include <QGridLayout>
#include <QHeaderView>
#include <QLineEdit>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QKeySequenceEdit>
#include <QKeyEvent>
#include <QApplication>
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 ) {
const auto msg = StringStream( "The command <b>", name, "</b> is already assigned to the key <b>", accelerator, "</b>.<br><br>",
"Do you want to unassign <b>", name, "</b> first?" );
const EMessageBoxReturn r = qt_MessageBox( tree->window(), msg, "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<QKeyEvent*>( 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 = StringStream( SettingsPath_get(), "commandlist.txt" );
globalOutputStream() << "Writing the command list to " << path << '\n';
TextFileOutputStream commandList( path );
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 ){
const auto strINI = StringStream( path, "shortcuts.ini" );
TextFileOutputStream file( strINI );
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 ){
const auto strINI = StringStream( path, "shortcuts.ini" );
FILE* f = fopen( strINI, "r" );
if ( f != 0 ) {
fclose( f );
globalOutputStream() << "loading custom shortcuts list from " << makeQuoted( strINI ) << '\n';
Version version = version_parse( COMMANDS_VERSION );
Version dataVersion = { 0, 0 };
{
char value[1024];
if ( read_var( strINI, "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 );
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 ) << '\n';
}
}