From 581dd79acb8616cc347636c6ca5432b550f80534 Mon Sep 17 00:00:00 2001 From: Garux Date: Sat, 30 Aug 2025 16:21:40 +0500 Subject: [PATCH] * build menu.customize.build variables: are editable --- docs/changelog-custom.txt | 1 + radiant/build.cpp | 331 +++++++++++++++++++++++++++----------- radiant/qe3.cpp | 4 +- radiant/qe3.h | 2 +- 4 files changed, 238 insertions(+), 100 deletions(-) diff --git a/docs/changelog-custom.txt b/docs/changelog-custom.txt index 749386c0..42ac2b5b 100644 --- a/docs/changelog-custom.txt +++ b/docs/changelog-custom.txt @@ -1132,6 +1132,7 @@ misc... * shader editor: make Copy and SelectAll binds work in read only mode * build menu.customize: drag and drop items; ctrl + drag = copy * build menu.customize: remove command line item when set empty + * build menu.customize.build variables: are editable diff --git a/radiant/build.cpp b/radiant/build.cpp index 86f34375..09dafaa0 100644 --- a/radiant/build.cpp +++ b/radiant/build.cpp @@ -27,6 +27,7 @@ #include "stream/stringstream.h" #include "versionlib.h" +#include "qe3.h" #include "mainframe.h" typedef std::map Variables; @@ -55,12 +56,12 @@ const char* build_get_variable( const char* name ){ class Evaluatable { public: - virtual ~Evaluatable(){} - virtual void evaluate( StringBuffer& output ) const = 0; + virtual Evaluatable* clone() const = 0; + virtual StringBuffer evaluate() const = 0; virtual void exportXML( XMLImporter& importer ) const = 0; }; -class VariableString : public Evaluatable +class VariableString final : public Evaluatable { CopiedString m_string; public: @@ -68,14 +69,17 @@ public: } VariableString( const char* string ) : m_string( string ){ } + Evaluatable* clone() const override { + return new VariableString( *this ); + } const char* c_str() const { return m_string.c_str(); } void setString( const char* string ){ m_string = string; } - void evaluate( StringBuffer& output ) const override { - StringBuffer variable; + StringBuffer evaluate() const override { + StringBuffer variable( 32 ), output( 256 ); bool in_variable = false; for ( const char* i = m_string.c_str(); *i != '\0'; ++i ) { @@ -105,65 +109,64 @@ public: } } } + return output; } void exportXML( XMLImporter& importer ) const override { importer << c_str(); } }; -class Conditional : public Evaluatable +class Conditional final : public Evaluatable { - VariableString* m_test; + VariableString m_test; public: - Evaluatable* m_result; - Conditional( VariableString* test ) : m_test( test ){ + VariableString m_result; + Conditional( VariableString&& test ) : m_test( test ){ } - ~Conditional(){ - delete m_test; - delete m_result; + Evaluatable* clone() const override { + return new Conditional( *this ); } - void evaluate( StringBuffer& output ) const override { - StringBuffer buffer; - m_test->evaluate( buffer ); - if ( !buffer.empty() ) { - m_result->evaluate( output ); + StringBuffer evaluate() const override { + if ( !m_test.evaluate().empty() ) { + return m_result.evaluate(); } + return StringBuffer(); } void exportXML( XMLImporter& importer ) const override { StaticElement conditionElement( "cond" ); - conditionElement.insertAttribute( "value", m_test->c_str() ); + conditionElement.insertAttribute( "value", m_test.c_str() ); importer.pushElement( conditionElement ); - m_result->exportXML( importer ); + m_result.exportXML( importer ); importer.popElement( conditionElement.name() ); } }; -typedef std::vector Evaluatables; +typedef std::vector> Evaluatables; -class Tool : public Evaluatable +class Tool { Evaluatables m_evaluatables; public: - ~Tool(){ - for ( auto* e : m_evaluatables ) - { - delete ( e ); - } + Tool() = default; + Tool( const Tool& other ){ + m_evaluatables.reserve( other.m_evaluatables.size() ); + for ( const auto& e : other.m_evaluatables ) + push_back( e->clone() ); } + Tool( Tool&& other ) noexcept = default; + Tool& operator=( Tool&& other ) noexcept = default; void push_back( Evaluatable* evaluatable ){ - m_evaluatables.push_back( evaluatable ); + m_evaluatables.emplace_back( evaluatable ); } - void evaluate( StringBuffer& output ) const override { - for ( const auto* e : m_evaluatables ) - { - e->evaluate( output ); - } + StringBuffer evaluate() const { + StringBuffer output( 256 ); + for ( const auto& e : m_evaluatables ) + output.push_string( e->evaluate().c_str() ); + return output; } - void exportXML( XMLImporter& importer ) const override { - for ( const auto* e : m_evaluatables ) - { + void exportXML( XMLImporter& importer ) const { + for ( const auto& e : m_evaluatables ) e->exportXML( importer ); - } } }; @@ -178,7 +181,7 @@ public: class VariableStringXMLConstructor final : public XMLElementParser { - StringBuffer m_buffer; + StringBuffer m_buffer{ 256 }; VariableString& m_variableString; public: VariableStringXMLConstructor( VariableString& variableString ) : m_variableString( variableString ){ @@ -186,43 +189,43 @@ public: ~VariableStringXMLConstructor(){ m_variableString.setString( m_buffer.c_str() ); } - std::size_t write( const char* buffer, std::size_t length ){ + std::size_t write( const char* buffer, std::size_t length ) override { m_buffer.push_range( buffer, buffer + length ); return length; } - XMLElementParser& pushElement( const XMLElement& element ){ + XMLElementParser& pushElement( const XMLElement& element ) override { ERROR_MESSAGE( "parse error: invalid element " << makeQuoted( element.name() ) ); return *this; } - void popElement( const char* name ){ + void popElement( const char* name ) override { } }; class ConditionalXMLConstructor final : public XMLElementParser { - StringBuffer m_buffer; + StringBuffer m_buffer{ 256 }; Conditional& m_conditional; public: ConditionalXMLConstructor( Conditional& conditional ) : m_conditional( conditional ){ } ~ConditionalXMLConstructor(){ - m_conditional.m_result = new VariableString( m_buffer.c_str() ); + m_conditional.m_result.setString( m_buffer.c_str() ); } - std::size_t write( const char* buffer, std::size_t length ){ + std::size_t write( const char* buffer, std::size_t length ) override { m_buffer.push_range( buffer, buffer + length ); return length; } - XMLElementParser& pushElement( const XMLElement& element ){ + XMLElementParser& pushElement( const XMLElement& element ) override { ERROR_MESSAGE( "parse error: invalid element " << makeQuoted( element.name() ) ); return *this; } - void popElement( const char* name ){ + void popElement( const char* name ) override { } }; class ToolXMLConstructor final : public XMLElementParser { - StringBuffer m_buffer; + StringBuffer m_buffer{ 256 }; Tool& m_tool; ConditionalXMLConstructor* m_conditional; public: @@ -231,14 +234,14 @@ public: ~ToolXMLConstructor(){ flush(); } - std::size_t write( const char* buffer, std::size_t length ){ + std::size_t write( const char* buffer, std::size_t length ) override { m_buffer.push_range( buffer, buffer + length ); return length; } - XMLElementParser& pushElement( const XMLElement& element ){ + XMLElementParser& pushElement( const XMLElement& element ) override { if ( string_equal( element.name(), "cond" ) ) { flush(); - Conditional* conditional = new Conditional( new VariableString( element.attribute( "value" ) ) ); + Conditional* conditional = new Conditional( VariableString( element.attribute( "value" ) ) ); m_tool.push_back( conditional ); m_conditional = new ConditionalXMLConstructor( *conditional ); return *m_conditional; @@ -249,7 +252,7 @@ public: return *this; } } - void popElement( const char* name ){ + void popElement( const char* name ) override { if ( string_equal( name, "cond" ) ) { delete m_conditional; } @@ -278,10 +281,10 @@ class BuildXMLConstructor final : public XMLElementParser public: BuildXMLConstructor( Build& build ) : m_build( build ){ } - std::size_t write( const char* buffer, std::size_t length ){ + std::size_t write( const char* buffer, std::size_t length ) override { return length; } - XMLElementParser& pushElement( const XMLElement& element ){ + XMLElementParser& pushElement( const XMLElement& element ) override { if ( string_equal( element.name(), "command" ) ) { m_build.push_back( BuildCommand() ); m_variableString = new VariableStringXMLConstructor( m_build.back() ); @@ -293,7 +296,7 @@ public: return *this; } } - void popElement( const char* name ){ + void popElement( const char* name ) override { delete m_variableString; } }; @@ -350,10 +353,10 @@ class ProjectXMLConstructor : public XMLElementParser public: ProjectXMLConstructor( Project& project, Tools& tools ) : m_project( project ), m_tools( tools ){ } - std::size_t write( const char* buffer, std::size_t length ){ + std::size_t write( const char* buffer, std::size_t length ) override { return length; } - XMLElementParser& pushElement( const XMLElement& element ){ + XMLElementParser& pushElement( const XMLElement& element ) override { if ( string_equal( element.name(), "var" ) ) { Tools::iterator i = m_tools.insert( Tools::value_type( element.attribute( "name" ), Tool() ) ).first; m_tool = new ToolXMLConstructor( ( *i ).second ); @@ -374,7 +377,7 @@ public: return *this; } } - void popElement( const char* name ){ + void popElement( const char* name ) override { if ( string_equal( name, "var" ) ) { delete m_tool; } @@ -387,13 +390,13 @@ public: class SkipAllParser : public XMLElementParser { public: - std::size_t write( const char* buffer, std::size_t length ){ + std::size_t write( const char* buffer, std::size_t length ) override { return length; } - XMLElementParser& pushElement( const XMLElement& element ){ + XMLElementParser& pushElement( const XMLElement& element ) override { return *this; } - void popElement( const char* name ){ + void popElement( const char* name ) override { } }; @@ -411,10 +414,10 @@ public: m_version( version_parse( version ) ), m_compatible( false ){ } - std::size_t write( const char* buffer, std::size_t length ){ + std::size_t write( const char* buffer, std::size_t length ) override { return length; } - XMLElementParser& pushElement( const XMLElement& element ){ + XMLElementParser& pushElement( const XMLElement& element ) override { if ( string_equal( element.name(), m_elementName.c_str() ) ) { Version dataVersion( version_parse( element.attribute( "version" ) ) ); if ( version_compatible( m_version, dataVersion ) ) { @@ -432,7 +435,7 @@ public: return *this; } } - void popElement( const char* name ){ + void popElement( const char* name ) override { } bool versionCompatible() const { @@ -443,9 +446,10 @@ public: namespace { Project g_build_project; +bool g_build_changed = false; Project::const_iterator g_lastExecutedBuild; Tools g_build_tools; -bool g_build_changed = false; +bool g_tools_changed = false; } void build_error_undefined_tool( const char* build, const char* tool ){ @@ -468,13 +472,16 @@ void project_verify( Project& project, Tools& tools ){ #endif } -std::vector build_construct_commands( size_t buildIdx ){ +void build_init_tools(){ for ( const auto& [ name, tool ] : g_build_tools ) { - StringBuffer output; - tool.evaluate( output ); - build_set_variable( name.c_str(), output.c_str() ); + build_set_variable( name.c_str(), tool.evaluate().c_str() ); } +} + +std::vector build_construct_commands( size_t buildIdx ){ + build_init_variables(); + build_init_tools(); std::vector commands; @@ -483,9 +490,7 @@ std::vector build_construct_commands( size_t buildIdx ){ g_lastExecutedBuild = buildIt; for ( const auto& command : buildIt->second ) { - StringBuffer output; - command.evaluate( output ); - if ( !output.empty() ) + if ( const StringBuffer output = command.evaluate(); !output.empty() ) commands.emplace_back( output.c_str() ); } } @@ -507,13 +512,13 @@ public: XMLParser( XMLElementParser& parser ){ m_stack.push_back( &parser ); } - std::size_t write( const char* buffer, std::size_t length ){ + std::size_t write( const char* buffer, std::size_t length ) override { return m_stack.back()->write( buffer, length ); } - void pushElement( const XMLElement& element ){ + void pushElement( const XMLElement& element ) override { m_stack.push_back( &m_stack.back()->pushElement( element ) ); } - void popElement( const char* name ){ + void popElement( const char* name ) override { m_stack.pop_back(); m_stack.back()->popElement( name ); } @@ -651,7 +656,7 @@ inline QTreeWidgetItem* new_item( const char *text ){ return item; } -#define LAST_ITER_STRING "+" +const char LAST_ITER_STRING[] = "+"; inline void last_iter_append( QTreeWidget* tree ){ QTreeWidgetItem *item = new_item( LAST_ITER_STRING ); item->setFlags( c_itemFlags ); @@ -762,7 +767,7 @@ void commands_cell_edited( QTreeWidgetItem *item ){ } else if ( !new_text.isEmpty() && !string_equal( new_text, LAST_ITER_STRING ) ) { // add new g_build_changed = true; - build.push_back( Build::value_type( VariableString( new_text ) ) ); + build.push_back( VariableString( new_text ) ); item->setFlags( c_itemFlagsDraggable ); // note: calls this function again with upper condition last_iter_append( item->treeWidget() ); @@ -941,8 +946,91 @@ protected: } }; -#include "qe3.h" #include "gtkutil/guisettings.h" +#include +#include +#include +#include +#include +#include +#include "stream/memstream.h" +#include "gtkutil/image.h" + +class CustomDelegate0 : public QStyledItemDelegate +{ +public: + void setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const override { + if ( QLineEdit *lineEdit = qobject_cast( editor ) ) { + const auto text = lineEdit->text().toLatin1(); + if( text != LAST_ITER_STRING && !g_build_tools.contains( text.constData() ) ){ // somethig new + const auto oldtext = index.data().toString().toLatin1(); + if( oldtext == LAST_ITER_STRING ){ // new tool + g_build_tools[ text.constData() ]; // insert + g_tools_changed = true; + model->setData( index, lineEdit->text(), Qt::ItemDataRole::DisplayRole ); + // add new last row + model->insertRow( model->rowCount() ); + model->setData( model->index( model->rowCount() - 1, 0 ), LAST_ITER_STRING, Qt::ItemDataRole::DisplayRole ); + } + else{ // new name + auto tool = g_build_tools.extract( oldtext.constData() ); + tool.key() = text.constData(); + g_build_tools.insert( std::move( tool ) ); + g_tools_changed = true; + model->setData( index, lineEdit->text(), Qt::ItemDataRole::DisplayRole ); + } + } + } + } +}; + +// Custom delegate to handle different display and edit text +class CustomDelegate1 : public QStyledItemDelegate +{ +public: + QWidget* createEditor( QWidget *parent, const QStyleOptionViewItem& option, const QModelIndex& index ) const override { + return index.siblingAtColumn( 0 ).data().toString() == LAST_ITER_STRING + ? nullptr // disable editing of special field + : QStyledItemDelegate::createEditor( parent, option, index ); + } + // Set the editor's data (xml from Qt::UserRole) + void setEditorData( QWidget *editor, const QModelIndex &index ) const override { + if ( QLineEdit *lineEdit = qobject_cast( editor ) ) { + lineEdit->setText( index.data( Qt::ItemDataRole::UserRole ).toString() ); + } + } + // Save the edited data back to Qt::UserRole and update Qt::DisplayRole + void setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const override { + if ( QLineEdit *lineEdit = qobject_cast( editor ) ) { + model->setData( index, lineEdit->text(), Qt::ItemDataRole::UserRole ); + // Optionally update display text based on edit text + const auto text = ( "" + lineEdit->text() + "" ).toLatin1(); + BufferInputStream projectLine( text, text.length() ); + XMLStreamParser parser( projectLine ); + Project project; + Tools tools; + ProjectXMLConstructor constructor( project, tools ); + XMLParser importer( constructor ); + + parser.exportXML( importer ); + + build_init_variables(); + const auto thisname = index.siblingAtColumn( 0 ).data().toString().toLatin1(); + for ( const auto& [ name, tool ] : g_build_tools ) + { + if( string_equal( name.c_str(), thisname.constData() ) ) // preceding tools are usable inside current one + break; + build_set_variable( name.c_str(), tool.evaluate().c_str() ); + } + + Tool& newtool = tools.begin()->second; + model->setData( index, newtool.evaluate().c_str(), Qt::ItemDataRole::DisplayRole ); + g_build_tools[ thisname.constData() ] = std::move( newtool ); + g_tools_changed = true; + build_clear_variables(); + } + } +}; EMessageBoxReturn BuildMenuDialog_construct( ProjectList& projectList ){ static auto [dialog, rootLayout] = [](){ @@ -1004,28 +1092,77 @@ EMessageBoxReturn BuildMenuDialog_construct( ProjectList& projectList ){ } } { - auto expander = new QGroupBox( "build variables" ); + auto expander = new QGroupBox( "Build Variables" ); expander->setFlat( true ); expander->setCheckable( true ); expander->setChecked( false ); grid->addWidget( expander, 2, 0 ); - bsp_init(); - for ( auto& [ name, tool ] : g_build_tools ){ - StringBuffer output; - tool.evaluate( output ); - build_set_variable( name.c_str(), output.c_str() ); + auto *containerWidget = new QWidget; + ( new QVBoxLayout( expander ) )->addWidget( containerWidget ); + containerWidget->hide(); + QObject::connect( expander, &QGroupBox::clicked, containerWidget, &QWidget::setVisible ); + auto *vbox = new QVBoxLayout( containerWidget ); + + build_init_variables(); + { // immutable variables + QIcon icon = new_local_icon( "copy.png" ); + auto *table = new QTableWidget( 0, 2 ); + vbox->addWidget( table ); + table->setEditTriggers( QAbstractItemView::EditTrigger::NoEditTriggers ); + table->setSelectionMode( QAbstractItemView::SelectionMode::SingleSelection ); + table->horizontalHeader()->setStretchLastSection( true ); + table->horizontalHeader()->setSectionResizeMode( 0, QHeaderView::ResizeMode::ResizeToContents ); + table->verticalHeader()->setSectionResizeMode( QHeaderView::ResizeMode::ResizeToContents ); + table->setSizeAdjustPolicy( QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents ); + table->setVerticalScrollBarPolicy( Qt::ScrollBarPolicy::ScrollBarAlwaysOff ); + table->horizontalHeader()->hide(); + table->verticalHeader()->hide(); + + QObject::connect( table, &QTableWidget::itemClicked, []( QTableWidgetItem *item ){ + if( item->column() == 0 ){ + QApplication::clipboard()->setText( item->text() ); + QTableWidget *table = item->tableWidget(); + QToolTip::showText( table->mapToGlobal( table->visualItemRect( item ).bottomLeft() ), "Copied to clipboard.", table, QRect(), 666 ); + } + } ); + + for( const auto& [ name, var ] : g_build_variables ){ + table->insertRow( table->rowCount() ); + table->setItem( table->rowCount() - 1, 0, new QTableWidgetItem( icon, '[' + QString( name.c_str() ) + ']' ) ); + table->setItem( table->rowCount() - 1, 1, new QTableWidgetItem( var.c_str() ) ); + } + } - StringOutputStream stream( 256 ); - for( const auto& [ name, var ] : g_build_variables ){ - stream << '[' << name << "] = " << var << '\n'; + build_init_tools(); + { // mutable 'Tool' variables + auto *table = new QTableWidget( 0, 2 ); + vbox->addWidget( table ); + table->setHorizontalHeaderLabels( { "Name", "Variable" } ); + table->setSelectionMode( QAbstractItemView::SelectionMode::SingleSelection ); + table->horizontalHeader()->setStretchLastSection( true ); + table->setWordWrap( true ); + table->verticalHeader()->setSectionResizeMode( QHeaderView::ResizeMode::ResizeToContents ); + table->setItemDelegateForColumn( 0, new CustomDelegate0 ); + table->setItemDelegateForColumn( 1, new CustomDelegate1 ); + + for ( auto& [ name, tool ] : g_build_tools ){ + table->insertRow( table->rowCount() ); + table->setItem( table->rowCount() - 1, 0, new QTableWidgetItem( name.c_str() ) ); + + auto *item = new QTableWidgetItem( g_build_variables[ name ].c_str() ); + table->setItem( table->rowCount() - 1, 1, item ); + StringOutputStream stream( 256 ); // store xml representation + { + XMLStreamWriter writer( stream ); // destructor dumps to stream + tool.exportXML( writer ); + } + item->setData( Qt::ItemDataRole::UserRole, strchr( stream, '>' ) + 1 ); // skip xml header + } + table->insertRow( table->rowCount() ); + table->setItem( table->rowCount() - 1, 0, new QTableWidgetItem( LAST_ITER_STRING ) ); } build_clear_variables(); - - auto label = new QLabel( stream.c_str() ); - label->hide(); - ( new QHBoxLayout( expander ) )->addWidget( label ); - QObject::connect( expander, &QGroupBox::clicked, label, &QWidget::setVisible ); } } @@ -1040,15 +1177,17 @@ void LoadBuildMenu(); void DoBuildMenu(){ ProjectList projectList( g_build_project ); - const Project bakproj = g_build_project; - const size_t baklast = Project_find( g_build_project, g_lastExecutedBuild ); + Project bakProj = g_build_project; + const size_t bakLastIdx = Project_find( g_build_project, g_lastExecutedBuild ); + Tools bakTools( g_build_tools ); const EMessageBoxReturn ret = BuildMenuDialog_construct( projectList ); if ( ret == eIDCANCEL || ret == 0 ) { - if ( projectList.m_changed || g_build_changed ){ - g_build_project = bakproj; - g_lastExecutedBuild = std::next( g_build_project.cbegin(), baklast ); + if ( projectList.m_changed || g_build_changed || g_tools_changed ){ + g_build_project.swap( bakProj ); + g_lastExecutedBuild = std::next( g_build_project.cbegin(), bakLastIdx ); + g_build_tools.swap( bakTools ); Build_refreshMenu( g_bsp_menu ); } } @@ -1058,7 +1197,7 @@ void DoBuildMenu(){ Build_refreshMenu( g_bsp_menu ); } - else if ( projectList.m_changed ) { + else if ( projectList.m_changed || g_tools_changed ) { g_build_changed = true; } } diff --git a/radiant/qe3.cpp b/radiant/qe3.cpp index 814e6fd4..1f5f5be3 100644 --- a/radiant/qe3.cpp +++ b/radiant/qe3.cpp @@ -154,7 +154,7 @@ bool ConfirmModified( const char* title ){ return true; // QMessageBox::StandardButton::Discard } -void bsp_init(){ +void build_init_variables(){ StringOutputStream stream( 256 ); build_set_variable( "RadiantPath", AppPath_get() ); @@ -226,8 +226,6 @@ void RunBSP( size_t buildIdx ){ Pointfile_Delete(); - bsp_init(); - const std::vector commands = build_construct_commands( buildIdx ); const bool monitor = std::any_of( commands.cbegin(), commands.cend(), []( const CopiedString& command ){ return strstr( command.c_str(), RADIANT_MONITOR_ADDRESS ) != 0; diff --git a/radiant/qe3.h b/radiant/qe3.h index 1cff9715..505bcb77 100644 --- a/radiant/qe3.h +++ b/radiant/qe3.h @@ -61,4 +61,4 @@ extern SimpleCounter g_brushCount; extern SimpleCounter g_patchCount; extern SimpleCounter g_entityCount; -void bsp_init(); +void build_init_variables();