980a7fe9b94379ac1ae620fe420b06035acdc4ca
[xonotic/netradiant.git] / radiant / build.cpp
1 /*
2    Copyright (C) 2001-2006, William Joseph.
3    All Rights Reserved.
4
5    This file is part of GtkRadiant.
6
7    GtkRadiant is free software; you can redistribute it and/or modify
8    it under the terms of the GNU General Public License as published by
9    the Free Software Foundation; either version 2 of the License, or
10    (at your option) any later version.
11
12    GtkRadiant is distributed in the hope that it will be useful,
13    but WITHOUT ANY WARRANTY; without even the implied warranty of
14    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15    GNU General Public License for more details.
16
17    You should have received a copy of the GNU General Public License
18    along with GtkRadiant; if not, write to the Free Software
19    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
20  */
21
22 #include "build.h"
23 #include "debugging/debugging.h"
24
25 #include <gtk/gtk.h>
26 #include <map>
27 #include <list>
28 #include "stream/stringstream.h"
29 #include "versionlib.h"
30
31 #include "mainframe.h"
32
33 typedef std::map<CopiedString, CopiedString> Variables;
34 Variables g_build_variables;
35
36 void build_clear_variables(){
37         g_build_variables.clear();
38 }
39
40 void build_set_variable( const char* name, const char* value ){
41         g_build_variables[name] = value;
42 }
43
44 const char* build_get_variable( const char* name ){
45         Variables::iterator i = g_build_variables.find( name );
46         if ( i != g_build_variables.end() ) {
47                 return ( *i ).second.c_str();
48         }
49         globalErrorStream() << "undefined build variable: " << makeQuoted( name ) << "\n";
50         return "";
51 }
52
53 #include "xml/ixml.h"
54 #include "xml/xmlelement.h"
55
56 class Evaluatable
57 {
58 public:
59 virtual ~Evaluatable() = default;
60 virtual void evaluate( StringBuffer& output ) = 0;
61 virtual void exportXML( XMLImporter& importer ) = 0;
62 };
63
64 class VariableString : public Evaluatable
65 {
66 CopiedString m_string;
67 public:
68 VariableString() : m_string(){
69 }
70 VariableString( const char* string ) : m_string( string ){
71 }
72 const char* c_str() const {
73         return m_string.c_str();
74 }
75 void setString( const char* string ){
76         m_string = string;
77 }
78
79 void evaluate( StringBuffer& output ){
80         // replace ".[ExecutableType]" with "[ExecutableExt]"
81         {
82                 StringBuffer output;
83                 const char pattern[] = ".[ExecutableType]";
84                 const char *string = m_string.c_str();
85                 for ( const char *i = string; *i != '\0'; i++ )
86                 {
87                         if ( strncmp( i, pattern, sizeof( pattern ) - 1 ) == 0 )
88                         {
89                                 output.push_string("[ExecutableExt]");
90                                 // minus 1 because \0, minus 1 because i++ in a replacement
91                                 i += sizeof( pattern ) - 2;
92                         }
93                         else
94                         {
95                                 output.push_back(*i);
96                         }
97                 }
98                 setString(output.c_str());
99         }
100
101         // add missing [ExtraQ3map2Args] if "[RadiantPath]q3map2[ExecutableExt]"
102         {
103                 const char pattern[] = "\"[RadiantPath]q3map2[ExecutableExt]\"";
104                 const char extra[] = "[ExtraQ3map2Args]";
105                 const char *string = m_string.c_str();
106                 if ( strstr( string, pattern ) != NULL && strstr( string, extra ) == NULL )
107                 {
108                         StringBuffer output;
109                         for ( const char *i = string; *i != '\0'; i++ )
110                         {
111                                 if ( strncmp( i, pattern, sizeof( pattern ) - 1 ) == 0 )
112                                 {
113                                         output.push_string(pattern);
114                                         output.push_string(" ");
115                                         output.push_string(extra);
116                                         // minus 1 because \0, no replacement
117                                         i += strlen( pattern ) - 1;
118                                 }
119                                 else
120                                 {
121                                         output.push_back(*i);
122                                 }
123                         }
124                         setString(output.c_str());
125                 }
126         }
127
128         StringBuffer variable;
129         bool in_variable = false;
130         for ( const char* i = m_string.c_str(); *i != '\0'; ++i )
131         {
132                 if ( !in_variable ) {
133                         switch ( *i )
134                         {
135                         case '[':
136                                 in_variable = true;
137                                 break;
138                         default:
139                                 output.push_back( *i );
140                                 break;
141                         }
142                 }
143                 else
144                 {
145                         switch ( *i )
146                         {
147                         case ']':
148                                 in_variable = false;
149                                 output.push_string( build_get_variable( variable.c_str() ) );
150                                 variable.clear();
151                                 break;
152                         default:
153                                 variable.push_back( *i );
154                                 break;
155                         }
156                 }
157         }
158 }
159 void exportXML( XMLImporter& importer ){
160         importer << c_str();
161 }
162 };
163
164 class Conditional : public Evaluatable
165 {
166 VariableString* m_test;
167 public:
168 Evaluatable* m_result;
169 Conditional( VariableString* test ) : m_test( test ){
170 }
171 ~Conditional(){
172         delete m_test;
173         delete m_result;
174 }
175 void evaluate( StringBuffer& output ){
176         StringBuffer buffer;
177         m_test->evaluate( buffer );
178         if ( !string_empty( buffer.c_str() ) ) {
179                 m_result->evaluate( output );
180         }
181 }
182 void exportXML( XMLImporter& importer ){
183         StaticElement conditionElement( "cond" );
184         conditionElement.insertAttribute( "value", m_test->c_str() );
185         importer.pushElement( conditionElement );
186         m_result->exportXML( importer );
187         importer.popElement( conditionElement.name() );
188 }
189 };
190
191 typedef std::vector<Evaluatable*> Evaluatables;
192
193 class Tool : public Evaluatable
194 {
195 Evaluatables m_evaluatables;
196 public:
197 ~Tool(){
198         for ( Evaluatables::iterator i = m_evaluatables.begin(); i != m_evaluatables.end(); ++i )
199         {
200                 delete ( *i );
201         }
202 }
203 void push_back( Evaluatable* evaluatable ){
204         m_evaluatables.push_back( evaluatable );
205 }
206 void evaluate( StringBuffer& output ){
207         for ( Evaluatables::iterator i = m_evaluatables.begin(); i != m_evaluatables.end(); ++i )
208         {
209                 ( *i )->evaluate( output );
210         }
211 }
212 void exportXML( XMLImporter& importer ){
213         for ( Evaluatables::iterator i = m_evaluatables.begin(); i != m_evaluatables.end(); ++i )
214         {
215                 ( *i )->exportXML( importer );
216         }
217 }
218 };
219
220 #include "xml/ixml.h"
221
222 class XMLElementParser : public TextOutputStream
223 {
224 public:
225 virtual ~XMLElementParser() = default;
226 virtual XMLElementParser& pushElement( const XMLElement& element ) = 0;
227 virtual void popElement( const char* name ) = 0;
228 };
229
230 class VariableStringXMLConstructor : public XMLElementParser
231 {
232 StringBuffer m_buffer;
233 VariableString& m_variableString;
234 public:
235 VariableStringXMLConstructor( VariableString& variableString ) : m_variableString( variableString ){
236 }
237 ~VariableStringXMLConstructor(){
238         m_variableString.setString( m_buffer.c_str() );
239 }
240 std::size_t write( const char* buffer, std::size_t length ){
241         m_buffer.push_range( buffer, buffer + length );
242         return length;
243 }
244 XMLElementParser& pushElement( const XMLElement& element ){
245         ERROR_MESSAGE( "parse error: invalid element \"" << element.name() << "\"" );
246         return *this;
247 }
248 void popElement( const char* name ){
249 }
250 };
251
252 class ConditionalXMLConstructor : public XMLElementParser
253 {
254 StringBuffer m_buffer;
255 Conditional& m_conditional;
256 public:
257 ConditionalXMLConstructor( Conditional& conditional ) : m_conditional( conditional ){
258 }
259 ~ConditionalXMLConstructor(){
260         m_conditional.m_result = new VariableString( m_buffer.c_str() );
261 }
262 std::size_t write( const char* buffer, std::size_t length ){
263         m_buffer.push_range( buffer, buffer + length );
264         return length;
265 }
266 XMLElementParser& pushElement( const XMLElement& element ){
267         ERROR_MESSAGE( "parse error: invalid element \"" << element.name() << "\"" );
268         return *this;
269 }
270 void popElement( const char* name ){
271 }
272 };
273
274 class ToolXMLConstructor : public XMLElementParser
275 {
276 StringBuffer m_buffer;
277 Tool& m_tool;
278 ConditionalXMLConstructor* m_conditional;
279 public:
280 ToolXMLConstructor( Tool& tool ) : m_tool( tool ){
281 }
282 ~ToolXMLConstructor(){
283         flush();
284 }
285 std::size_t write( const char* buffer, std::size_t length ){
286         m_buffer.push_range( buffer, buffer + length );
287         return length;
288 }
289 XMLElementParser& pushElement( const XMLElement& element ){
290         if ( string_equal( element.name(), "cond" ) ) {
291                 flush();
292                 Conditional* conditional = new Conditional( new VariableString( element.attribute( "value" ) ) );
293                 m_tool.push_back( conditional );
294                 m_conditional = new ConditionalXMLConstructor( *conditional );
295                 return *m_conditional;
296         }
297         else
298         {
299                 ERROR_MESSAGE( "parse error: invalid element \"" << element.name() << "\"" );
300                 return *this;
301         }
302 }
303 void popElement( const char* name ){
304         if ( string_equal( name, "cond" ) ) {
305                 delete m_conditional;
306         }
307 }
308
309 void flush(){
310         if ( !m_buffer.empty() ) {
311                 m_tool.push_back( new VariableString( m_buffer.c_str() ) );
312                 m_buffer.clear();
313         }
314 }
315 };
316
317 typedef VariableString BuildCommand;
318 typedef std::list<BuildCommand> Build;
319
320 class BuildXMLConstructor : public XMLElementParser
321 {
322 VariableStringXMLConstructor* m_variableString;
323 Build& m_build;
324 public:
325 BuildXMLConstructor( Build& build ) : m_build( build ){
326 }
327 std::size_t write( const char* buffer, std::size_t length ){
328         return length;
329 }
330 XMLElementParser& pushElement( const XMLElement& element ){
331         if ( string_equal( element.name(), "command" ) ) {
332                 m_build.push_back( BuildCommand() );
333                 m_variableString = new VariableStringXMLConstructor( m_build.back() );
334                 return *m_variableString;
335         }
336         else
337         {
338                 ERROR_MESSAGE( "parse error: invalid element" );
339                 return *this;
340         }
341 }
342 void popElement( const char* name ){
343         delete m_variableString;
344 }
345 };
346
347 typedef std::pair<CopiedString, Build> BuildPair;
348 const char *SEPARATOR_STRING = "-";
349 static bool is_separator( const BuildPair &p ){
350         if ( !string_equal( p.first.c_str(), SEPARATOR_STRING ) ) {
351                 return false;
352         }
353         for ( Build::const_iterator j = p.second.begin(); j != p.second.end(); ++j )
354         {
355                 if ( !string_equal( ( *j ).c_str(), "" ) ) {
356                         return false;
357                 }
358         }
359         return true;
360 }
361
362
363 typedef std::list<BuildPair> Project;
364
365 Project::iterator Project_find( Project& project, const char* name ){
366         return std::find_if(project.begin(), project.end(), [&](const BuildPair &self) {
367                 return string_equal(self.first.c_str(), name);
368         });
369 }
370
371 Project::iterator Project_find( Project& project, std::size_t index ){
372         Project::iterator i = project.begin();
373         while ( index-- != 0 && i != project.end() )
374         {
375                 ++i;
376         }
377         return i;
378 }
379
380 Build& project_find( Project& project, const char* build ){
381         Project::iterator i = Project_find( project, build );
382         ASSERT_MESSAGE( i != project.end(), "error finding build command" );
383         return ( *i ).second;
384 }
385
386 Build::iterator Build_find( Build& build, std::size_t index ){
387         Build::iterator i = build.begin();
388         while ( index-- != 0 && i != build.end() )
389         {
390                 ++i;
391         }
392         return i;
393 }
394
395 typedef std::map<CopiedString, Tool> Tools;
396
397 class ProjectXMLConstructor : public XMLElementParser
398 {
399 ToolXMLConstructor* m_tool;
400 BuildXMLConstructor* m_build;
401 Project& m_project;
402 Tools& m_tools;
403 public:
404 ProjectXMLConstructor( Project& project, Tools& tools ) : m_project( project ), m_tools( tools ){
405 }
406 std::size_t write( const char* buffer, std::size_t length ){
407         return length;
408 }
409 XMLElementParser& pushElement( const XMLElement& element ){
410         if ( string_equal( element.name(), "var" ) ) {
411                 Tools::iterator i = m_tools.insert( Tools::value_type( element.attribute( "name" ), Tool() ) ).first;
412                 m_tool = new ToolXMLConstructor( ( *i ).second );
413                 return *m_tool;
414         }
415         else if ( string_equal( element.name(), "build" ) ) {
416                 m_project.push_back( Project::value_type( element.attribute( "name" ), Build() ) );
417                 m_build = new BuildXMLConstructor( m_project.back().second );
418                 return *m_build;
419         }
420         else if ( string_equal( element.name(), "separator" ) ) {
421                 m_project.push_back( Project::value_type( SEPARATOR_STRING, Build() ) );
422                 return *this;
423         }
424         else
425         {
426                 ERROR_MESSAGE( "parse error: invalid element" );
427                 return *this;
428         }
429 }
430 void popElement( const char* name ){
431         if ( string_equal( name, "var" ) ) {
432                 delete m_tool;
433         }
434         else if ( string_equal( name, "build" ) ) {
435                 delete m_build;
436         }
437 }
438 };
439
440 class SkipAllParser : public XMLElementParser
441 {
442 public:
443 std::size_t write( const char* buffer, std::size_t length ){
444         return length;
445 }
446 XMLElementParser& pushElement( const XMLElement& element ){
447         return *this;
448 }
449 void popElement( const char* name ){
450 }
451 };
452
453 class RootXMLConstructor : public XMLElementParser
454 {
455 CopiedString m_elementName;
456 XMLElementParser& m_parser;
457 SkipAllParser m_skip;
458 Version m_version;
459 bool m_compatible;
460 public:
461 RootXMLConstructor( const char* elementName, XMLElementParser& parser, const char* version ) :
462         m_elementName( elementName ),
463         m_parser( parser ),
464         m_version( version_parse( version ) ),
465         m_compatible( false ){
466 }
467 std::size_t write( const char* buffer, std::size_t length ){
468         return length;
469 }
470 XMLElementParser& pushElement( const XMLElement& element ){
471         if ( string_equal( element.name(), m_elementName.c_str() ) ) {
472                 Version dataVersion( version_parse( element.attribute( "version" ) ) );
473                 if ( version_compatible( m_version, dataVersion ) ) {
474                         m_compatible = true;
475                         return m_parser;
476                 }
477                 else
478                 {
479                         return m_skip;
480                 }
481         }
482         else
483         {
484                 //ERROR_MESSAGE("parse error: invalid element \"" << element.name() << "\"");
485                 return *this;
486         }
487 }
488 void popElement( const char* name ){
489 }
490
491 bool versionCompatible() const {
492         return m_compatible;
493 }
494 };
495
496 namespace
497 {
498 Project g_build_project;
499 Tools g_build_tools;
500 bool g_build_changed = false;
501 }
502
503 void build_error_undefined_tool( const char* build, const char* tool ){
504         globalErrorStream() << "build " << makeQuoted( build ) << " refers to undefined tool " << makeQuoted( tool ) << '\n';
505 }
506
507 void project_verify( Project& project, Tools& tools ){
508 #if 0
509         for ( Project::iterator i = project.begin(); i != project.end(); ++i )
510         {
511                 Build& build = ( *i ).second;
512                 for ( Build::iterator j = build.begin(); j != build.end(); ++j )
513                 {
514                         Tools::iterator k = tools.find( ( *j ).first );
515                         if ( k == g_build_tools.end() ) {
516                                 build_error_undefined_tool( ( *i ).first.c_str(), ( *j ).first.c_str() );
517                         }
518                 }
519         }
520 #endif
521 }
522
523 void build_run( const char* name, CommandListener& listener ){
524         for ( Tools::iterator i = g_build_tools.begin(); i != g_build_tools.end(); ++i )
525         {
526                 StringBuffer output;
527                 ( *i ).second.evaluate( output );
528                 build_set_variable( ( *i ).first.c_str(), output.c_str() );
529         }
530
531         {
532                 Project::iterator i = Project_find( g_build_project, name );
533                 if ( i != g_build_project.end() ) {
534                         Build& build = ( *i ).second;
535                         for ( Build::iterator j = build.begin(); j != build.end(); ++j )
536                         {
537                                 StringBuffer output;
538                                 ( *j ).evaluate( output );
539                                 listener.execute( output.c_str() );
540                         }
541                 }
542                 else
543                 {
544                         globalErrorStream() << "build " << makeQuoted( name ) << " not defined";
545                 }
546         }
547 }
548
549
550 typedef std::vector<XMLElementParser*> XMLElementStack;
551
552 class XMLParser : public XMLImporter
553 {
554 XMLElementStack m_stack;
555 public:
556 XMLParser( XMLElementParser& parser ){
557         m_stack.push_back( &parser );
558 }
559 std::size_t write( const char* buffer, std::size_t length ){
560         return m_stack.back()->write( buffer, length );
561 }
562 void pushElement( const XMLElement& element ){
563         m_stack.push_back( &m_stack.back()->pushElement( element ) );
564 }
565 void popElement( const char* name ){
566         m_stack.pop_back();
567         m_stack.back()->popElement( name );
568 }
569 };
570
571 #include "stream/textfilestream.h"
572 #include "xml/xmlparser.h"
573
574 const char* const BUILDMENU_VERSION = "2.0";
575
576 bool build_commands_parse( const char* filename ){
577         TextFileInputStream projectFile( filename );
578         if ( !projectFile.failed() ) {
579                 ProjectXMLConstructor projectConstructor( g_build_project, g_build_tools );
580                 RootXMLConstructor rootConstructor( "project", projectConstructor, BUILDMENU_VERSION );
581                 XMLParser importer( rootConstructor );
582                 XMLStreamParser parser( projectFile );
583                 parser.exportXML( importer );
584
585                 if ( rootConstructor.versionCompatible() ) {
586                         project_verify( g_build_project, g_build_tools );
587
588                         return true;
589                 }
590                 globalErrorStream() << "failed to parse build menu: " << makeQuoted( filename ) << "\n";
591         }
592         return false;
593 }
594
595 void build_commands_clear(){
596         g_build_project.clear();
597         g_build_tools.clear();
598 }
599
600 class BuildXMLExporter
601 {
602 Build& m_build;
603 public:
604 BuildXMLExporter( Build& build ) : m_build( build ){
605 }
606 void exportXML( XMLImporter& importer ){
607         importer << "\n";
608         for ( Build::iterator i = m_build.begin(); i != m_build.end(); ++i )
609         {
610                 StaticElement commandElement( "command" );
611                 importer.pushElement( commandElement );
612                 ( *i ).exportXML( importer );
613                 importer.popElement( commandElement.name() );
614                 importer << "\n";
615         }
616 }
617 };
618
619 class ProjectXMLExporter
620 {
621 Project& m_project;
622 Tools& m_tools;
623 public:
624 ProjectXMLExporter( Project& project, Tools& tools ) : m_project( project ), m_tools( tools ){
625 }
626 void exportXML( XMLImporter& importer ){
627         StaticElement projectElement( "project" );
628         projectElement.insertAttribute( "version", BUILDMENU_VERSION );
629         importer.pushElement( projectElement );
630         importer << "\n";
631
632         for ( Tools::iterator i = m_tools.begin(); i != m_tools.end(); ++i )
633         {
634                 StaticElement toolElement( "var" );
635                 toolElement.insertAttribute( "name", ( *i ).first.c_str() );
636                 importer.pushElement( toolElement );
637                 ( *i ).second.exportXML( importer );
638                 importer.popElement( toolElement.name() );
639                 importer << "\n";
640         }
641         for ( Project::iterator i = m_project.begin(); i != m_project.end(); ++i )
642         {
643                 if ( is_separator( *i ) ) {
644                         StaticElement buildElement( "separator" );
645                         importer.pushElement( buildElement );
646                         importer.popElement( buildElement.name() );
647                         importer << "\n";
648                 }
649                 else
650                 {
651                         StaticElement buildElement( "build" );
652                         buildElement.insertAttribute( "name", ( *i ).first.c_str() );
653                         importer.pushElement( buildElement );
654                         BuildXMLExporter buildExporter( ( *i ).second );
655                         buildExporter.exportXML( importer );
656                         importer.popElement( buildElement.name() );
657                         importer << "\n";
658                 }
659         }
660         importer.popElement( projectElement.name() );
661 }
662 };
663
664 #include "xml/xmlwriter.h"
665
666 void build_commands_write( const char* filename ){
667         TextFileOutputStream projectFile( filename );
668         if ( !projectFile.failed() ) {
669                 XMLStreamWriter writer( projectFile );
670                 ProjectXMLExporter projectExporter( g_build_project, g_build_tools );
671                 writer << "\n";
672                 projectExporter.exportXML( writer );
673                 writer << "\n";
674         }
675 }
676
677
678 #include <gdk/gdkkeysyms.h>
679
680 #include "gtkutil/dialog.h"
681 #include "gtkutil/closure.h"
682 #include "gtkutil/window.h"
683 #include "gtkdlgs.h"
684
685 void Build_refreshMenu( ui::Menu menu );
686
687
688 void BSPCommandList_Construct( ui::ListStore store, Project& project ){
689         store.clear();
690
691         for ( Project::iterator i = project.begin(); i != project.end(); ++i )
692         {
693                 store.append(0, (*i).first.c_str());
694         }
695
696         store.append();
697 }
698
699 class ProjectList
700 {
701 public:
702 Project& m_project;
703 ui::ListStore m_store{ui::null};
704 bool m_changed;
705 ProjectList( Project& project ) : m_project( project ), m_changed( false ){
706 }
707 };
708
709 gboolean project_cell_edited(ui::CellRendererText cell, gchar* path_string, gchar* new_text, ProjectList* projectList ){
710         Project& project = projectList->m_project;
711
712         auto path = ui::TreePath( path_string );
713
714         ASSERT_MESSAGE( gtk_tree_path_get_depth( path ) == 1, "invalid path length" );
715
716         GtkTreeIter iter;
717         gtk_tree_model_get_iter(projectList->m_store, &iter, path );
718
719         Project::iterator i = Project_find( project, gtk_tree_path_get_indices( path )[0] );
720         if ( i != project.end() ) {
721                 projectList->m_changed = true;
722                 if ( string_empty( new_text ) ) {
723                         project.erase( i );
724                         gtk_list_store_remove( projectList->m_store, &iter );
725                 }
726                 else
727                 {
728                         ( *i ).first = new_text;
729                         gtk_list_store_set( projectList->m_store, &iter, 0, new_text, -1 );
730                 }
731         }
732         else if ( !string_empty( new_text ) ) {
733                 projectList->m_changed = true;
734                 project.push_back( Project::value_type( new_text, Build() ) );
735
736                 gtk_list_store_set( projectList->m_store, &iter, 0, new_text, -1 );
737                 projectList->m_store.append();
738         }
739
740         gtk_tree_path_free( path );
741
742         Build_refreshMenu( g_bsp_menu );
743
744         return FALSE;
745 }
746
747 gboolean project_key_press( ui::TreeView widget, GdkEventKey* event, ProjectList* projectList ){
748         Project& project = projectList->m_project;
749
750         if ( event->keyval == GDK_KEY_Delete ) {
751                 auto selection = ui::TreeSelection::from(gtk_tree_view_get_selection(widget));
752                 GtkTreeIter iter;
753                 GtkTreeModel* model;
754                 if ( gtk_tree_selection_get_selected( selection, &model, &iter ) ) {
755                         auto path = gtk_tree_model_get_path( model, &iter );
756                         Project::iterator x = Project_find( project, gtk_tree_path_get_indices( path )[0] );
757                         gtk_tree_path_free( path );
758
759                         if ( x != project.end() ) {
760                                 projectList->m_changed = true;
761                                 project.erase( x );
762                                 Build_refreshMenu( g_bsp_menu );
763
764                                 gtk_list_store_remove( projectList->m_store, &iter );
765                         }
766                 }
767         }
768         return FALSE;
769 }
770
771
772 Build* g_current_build = 0;
773
774 gboolean project_selection_changed( ui::TreeSelection selection, ui::ListStore store ){
775         Project& project = g_build_project;
776
777         store.clear();
778
779         GtkTreeIter iter;
780         GtkTreeModel* model;
781         if ( gtk_tree_selection_get_selected( selection, &model, &iter ) ) {
782                 auto path = gtk_tree_model_get_path( model, &iter );
783                 Project::iterator x = Project_find( project, gtk_tree_path_get_indices( path )[0] );
784                 gtk_tree_path_free( path );
785
786                 if ( x != project.end() ) {
787                         Build& build = ( *x ).second;
788                         g_current_build = &build;
789
790                         for ( Build::iterator i = build.begin(); i != build.end(); ++i )
791                         {
792                                 store.append(0, (*i).c_str());
793                         }
794                         store.append();
795                 }
796                 else
797                 {
798                         g_current_build = 0;
799                 }
800         }
801         else
802         {
803                 g_current_build = 0;
804         }
805
806         return FALSE;
807 }
808
809 gboolean commands_cell_edited(ui::CellRendererText cell, gchar* path_string, gchar* new_text, ui::ListStore store ){
810         if ( g_current_build == 0 ) {
811                 return FALSE;
812         }
813         Build& build = *g_current_build;
814
815         auto path = ui::TreePath( path_string );
816
817         ASSERT_MESSAGE( gtk_tree_path_get_depth( path ) == 1, "invalid path length" );
818
819         GtkTreeIter iter;
820         gtk_tree_model_get_iter(store, &iter, path );
821
822         Build::iterator i = Build_find( build, gtk_tree_path_get_indices( path )[0] );
823         if ( i != build.end() ) {
824                 g_build_changed = true;
825                 ( *i ).setString( new_text );
826
827                 gtk_list_store_set( store, &iter, 0, new_text, -1 );
828         }
829         else if ( !string_empty( new_text ) ) {
830                 g_build_changed = true;
831                 build.push_back( Build::value_type( VariableString( new_text ) ) );
832
833                 gtk_list_store_set( store, &iter, 0, new_text, -1 );
834
835                 store.append();
836         }
837
838         gtk_tree_path_free( path );
839
840         Build_refreshMenu( g_bsp_menu );
841
842         return FALSE;
843 }
844
845 gboolean commands_key_press( ui::TreeView widget, GdkEventKey* event, ui::ListStore store ){
846         if ( g_current_build == 0 ) {
847                 return FALSE;
848         }
849         Build& build = *g_current_build;
850
851         if ( event->keyval == GDK_KEY_Delete ) {
852                 auto selection = gtk_tree_view_get_selection(widget );
853                 GtkTreeIter iter;
854                 GtkTreeModel* model;
855                 if ( gtk_tree_selection_get_selected( selection, &model, &iter ) ) {
856                         auto path = gtk_tree_model_get_path( model, &iter );
857                         Build::iterator i = Build_find( build, gtk_tree_path_get_indices( path )[0] );
858                         gtk_tree_path_free( path );
859
860                         if ( i != build.end() ) {
861                                 g_build_changed = true;
862                                 build.erase( i );
863
864                                 gtk_list_store_remove( store, &iter );
865                         }
866                 }
867         }
868         return FALSE;
869 }
870
871
872 ui::Window BuildMenuDialog_construct( ModalDialog& modal, ProjectList& projectList ){
873         ui::Window window = MainFrame_getWindow().create_dialog_window("Build Menu", G_CALLBACK(dialog_delete_callback ), &modal, -1, 400 );
874
875         // FIXME: GTK_WIN_POS_CENTER_ON_PARENT must be used instead but does not work
876         // for unknown reason.
877         // It's possible MaingFrame_getWindow() does not return the main window.
878         // It's known the preferences window has same issue when using MaingFrame_getWindow().
879         gtk_window_set_position( window, GTK_WIN_POS_CENTER_ALWAYS );
880
881         {
882                 auto table1 = create_dialog_table( 2, 2, 4, 4, 4 );
883                 window.add(table1);
884                 {
885                         auto vbox = create_dialog_vbox( 4 );
886             table1.attach(vbox, {1, 2, 0, 1}, {GTK_FILL, GTK_FILL});
887                         {
888                                 auto button = create_dialog_button( "OK", G_CALLBACK( dialog_button_ok ), &modal );
889                                 vbox.pack_start( button, FALSE, FALSE, 0 );
890                         }
891                         {
892                                 auto button = create_dialog_button( "Cancel", G_CALLBACK( dialog_button_cancel ), &modal );
893                                 vbox.pack_start( button, FALSE, FALSE, 0 );
894                         }
895                 }
896                 auto buildViewStore = ui::ListStore::from(gtk_list_store_new( 1, G_TYPE_STRING ));
897                 auto buildView = ui::TreeView( ui::TreeModel::from( buildViewStore._handle ));
898                 {
899                         auto frame = create_dialog_frame( "Build menu" );
900             table1.attach(frame, {0, 1, 0, 1});
901                         {
902                                 auto scr = create_scrolled_window( ui::Policy::NEVER, ui::Policy::AUTOMATIC, 4 );
903                                 frame.add(scr);
904
905                                 {
906                                         auto view = buildView;
907                                         auto store = buildViewStore;
908                                         gtk_tree_view_set_headers_visible(view, FALSE );
909
910                                         auto renderer = ui::CellRendererText(ui::New);
911                                         object_set_boolean_property( G_OBJECT( renderer ), "editable", TRUE );
912                                         renderer.connect("edited", G_CALLBACK( project_cell_edited ), &projectList );
913
914                                         auto column = ui::TreeViewColumn( "", renderer, {{"text", 0}} );
915                                         gtk_tree_view_append_column(view, column );
916
917                                         auto selection = gtk_tree_view_get_selection(view );
918                                         gtk_tree_selection_set_mode( selection, GTK_SELECTION_BROWSE );
919
920                                         view.show();
921
922                                         projectList.m_store = store;
923                                         scr.add(view);
924
925                                         view.connect( "key_press_event", G_CALLBACK( project_key_press ), &projectList );
926
927                                         store.unref();
928                                 }
929                         }
930                 }
931                 {
932                         auto frame = create_dialog_frame( "Command line" );
933             table1.attach(frame, {0, 1, 1, 2});
934                         {
935                                 auto scr = create_scrolled_window( ui::Policy::NEVER, ui::Policy::AUTOMATIC, 4 );
936                                 frame.add(scr);
937
938                                 {
939                                         auto store = ui::ListStore::from(gtk_list_store_new( 1, G_TYPE_STRING ));
940
941                                         auto view = ui::TreeView(ui::TreeModel::from( store._handle ));
942                                         gtk_tree_view_set_headers_visible(view, FALSE );
943
944                                         auto renderer = ui::CellRendererText(ui::New);
945                                         object_set_boolean_property( G_OBJECT( renderer ), "editable", TRUE );
946                                         renderer.connect( "edited", G_CALLBACK( commands_cell_edited ), store );
947
948                                         auto column = ui::TreeViewColumn( "", renderer, {{"text", 0}} );
949                                         gtk_tree_view_append_column(view, column );
950
951                                         auto selection = gtk_tree_view_get_selection(view );
952                                         gtk_tree_selection_set_mode( selection, GTK_SELECTION_BROWSE );
953
954                                         view.show();
955
956                                         scr.add(view);
957
958                                         store.unref();
959
960                                         view.connect( "key_press_event", G_CALLBACK( commands_key_press ), store );
961
962                                         auto sel = ui::TreeSelection::from(gtk_tree_view_get_selection(buildView ));
963                                         sel.connect( "changed", G_CALLBACK( project_selection_changed ), store );
964                                 }
965                         }
966                 }
967         }
968
969         BSPCommandList_Construct( projectList.m_store, g_build_project );
970
971         return window;
972 }
973
974 namespace
975 {
976 CopiedString g_buildMenu;
977 }
978
979 void LoadBuildMenu();
980
981 void DoBuildMenu(){
982         ModalDialog modal;
983
984         ProjectList projectList( g_build_project );
985
986         ui::Window window = BuildMenuDialog_construct( modal, projectList );
987
988         if ( modal_dialog_show( window, modal ) == eIDCANCEL ) {
989                 build_commands_clear();
990                 LoadBuildMenu();
991
992                 Build_refreshMenu( g_bsp_menu );
993         }
994         else if ( projectList.m_changed ) {
995                 g_build_changed = true;
996         }
997
998         window.destroy();
999 }
1000
1001
1002
1003 #include "gtkutil/menu.h"
1004 #include "mainframe.h"
1005 #include "preferences.h"
1006 #include "qe3.h"
1007
1008 class BuildMenuItem
1009 {
1010 const char* m_name;
1011 public:
1012 ui::MenuItem m_item;
1013 BuildMenuItem( const char* name, ui::MenuItem item )
1014         : m_name( name ), m_item( item ){
1015 }
1016 void run(){
1017         RunBSP( m_name );
1018 }
1019 typedef MemberCaller<BuildMenuItem, void(), &BuildMenuItem::run> RunCaller;
1020 };
1021
1022 typedef std::list<BuildMenuItem> BuildMenuItems;
1023 BuildMenuItems g_BuildMenuItems;
1024
1025
1026 ui::Menu g_bsp_menu{ui::null};
1027
1028 void Build_constructMenu( ui::Menu menu ){
1029         for ( Project::iterator i = g_build_project.begin(); i != g_build_project.end(); ++i )
1030         {
1031                 g_BuildMenuItems.push_back( BuildMenuItem( ( *i ).first.c_str(), ui::MenuItem(ui::null) ) );
1032                 if ( is_separator( *i ) ) {
1033                         g_BuildMenuItems.back().m_item = menu_separator( menu );
1034                 }
1035                 else
1036                 {
1037                         g_BuildMenuItems.back().m_item = create_menu_item_with_mnemonic( menu, ( *i ).first.c_str(), BuildMenuItem::RunCaller( g_BuildMenuItems.back() ) );
1038                 }
1039         }
1040 }
1041
1042
1043 void Build_refreshMenu( ui::Menu menu ){
1044         for (auto i = g_BuildMenuItems.begin(); i != g_BuildMenuItems.end(); ++i )
1045         {
1046                 menu.remove(ui::MenuItem(i->m_item));
1047         }
1048
1049         g_BuildMenuItems.clear();
1050
1051         Build_constructMenu( menu );
1052 }
1053
1054
1055 void LoadBuildMenu(){
1056         if ( string_empty( g_buildMenu.c_str() ) || !build_commands_parse( g_buildMenu.c_str() ) ) {
1057                 {
1058                         StringOutputStream buffer( 256 );
1059                         buffer << GameToolsPath_get() << "default_build_menu.xml";
1060
1061                         bool success = build_commands_parse( buffer.c_str() );
1062                         ASSERT_MESSAGE( success, "failed to parse default build commands: " << buffer.c_str() );
1063                 }
1064                 {
1065                         StringOutputStream buffer( 256 );
1066                         buffer << SettingsPath_get() << g_pGameDescription->mGameFile.c_str() << "/build_menu.xml";
1067
1068                         g_buildMenu = buffer.c_str();
1069                 }
1070         }
1071 }
1072
1073 void SaveBuildMenu(){
1074         if ( g_build_changed ) {
1075                 g_build_changed = false;
1076                 build_commands_write( g_buildMenu.c_str() );
1077         }
1078 }
1079
1080 #include "preferencesystem.h"
1081 #include "stringio.h"
1082
1083 void BuildMenu_Construct(){
1084         GlobalPreferenceSystem().registerPreference( "BuildMenu", make_property_string( g_buildMenu ) );
1085         LoadBuildMenu();
1086 }
1087 void BuildMenu_Destroy(){
1088         SaveBuildMenu();
1089 }