]> de.git.xonotic.org Git - xonotic/netradiant.git/blob - radiant/gtkdlgs.cpp
Merge commit '1a0075a3f03af095ee32ded7f101cac79267f906' into master-merge
[xonotic/netradiant.git] / radiant / gtkdlgs.cpp
1 /*
2    Copyright (c) 2001, Loki software, inc.
3    All rights reserved.
4
5    Redistribution and use in source and binary forms, with or without modification,
6    are permitted provided that the following conditions are met:
7
8    Redistributions of source code must retain the above copyright notice, this list
9    of conditions and the following disclaimer.
10
11    Redistributions in binary form must reproduce the above copyright notice, this
12    list of conditions and the following disclaimer in the documentation and/or
13    other materials provided with the distribution.
14
15    Neither the name of Loki software nor the names of its contributors may be used
16    to endorse or promote products derived from this software without specific prior
17    written permission.
18
19    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
20    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22    DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
23    DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26    ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 //
32 // Some small dialogs that don't need much
33 //
34 // Leonardo Zide (leo@lokigames.com)
35 //
36
37 #include "gtkdlgs.h"
38 #include "globaldefs.h"
39
40 #include <gtk/gtk.h>
41
42 #include "debugging/debugging.h"
43 #include "aboutmsg.h"
44
45 #include "igl.h"
46 #include "iscenegraph.h"
47 #include "iselection.h"
48
49 #include <gdk/gdkkeysyms.h>
50 #include <uilib/uilib.h>
51 #include <gtk/gtkspinbutton.h>
52
53 #include "os/path.h"
54 #include "math/aabb.h"
55 #include "container/array.h"
56 #include "generic/static.h"
57 #include "stream/stringstream.h"
58 #include "convert.h"
59 #include "gtkutil/messagebox.h"
60 #include "gtkutil/image.h"
61
62 #include "gtkmisc.h"
63 #include "brushmanip.h"
64 #include "build.h"
65 #include "qe3.h"
66 #include "texwindow.h"
67 #include "xywindow.h"
68 #include "mainframe.h"
69 #include "preferences.h"
70 #include "url.h"
71 #include "cmdlib.h"
72
73 #include "qerplugin.h"
74 #include "os/file.h"
75
76
77
78 // =============================================================================
79 // Project settings dialog
80
81 class GameComboConfiguration
82 {
83 public:
84 const char* basegame_dir;
85 const char* basegame;
86 const char* known_dir;
87 const char* known;
88 const char* custom;
89
90 GameComboConfiguration() :
91         basegame_dir( g_pGameDescription->getRequiredKeyValue( "basegame" ) ),
92         basegame( g_pGameDescription->getRequiredKeyValue( "basegamename" ) ),
93         known_dir( g_pGameDescription->getKeyValue( "knowngame" ) ),
94         known( g_pGameDescription->getKeyValue( "knowngamename" ) ),
95         custom( g_pGameDescription->getRequiredKeyValue( "unknowngamename" ) ){
96 }
97 };
98
99 typedef LazyStatic<GameComboConfiguration> LazyStaticGameComboConfiguration;
100
101 inline GameComboConfiguration& globalGameComboConfiguration(){
102         return LazyStaticGameComboConfiguration::instance();
103 }
104
105
106 struct gamecombo_t
107 {
108         gamecombo_t( int _game, const char* _fs_game, bool _sensitive )
109                 : game( _game ), fs_game( _fs_game ), sensitive( _sensitive )
110         {}
111         int game;
112         const char* fs_game;
113         bool sensitive;
114 };
115
116 gamecombo_t gamecombo_for_dir( const char* dir ){
117         if ( string_equal( dir, globalGameComboConfiguration().basegame_dir ) ) {
118                 return gamecombo_t( 0, "", false );
119         }
120         else if ( string_equal( dir, globalGameComboConfiguration().known_dir ) ) {
121                 return gamecombo_t( 1, dir, false );
122         }
123         else
124         {
125                 return gamecombo_t( string_empty( globalGameComboConfiguration().known_dir ) ? 1 : 2, dir, true );
126         }
127 }
128
129 gamecombo_t gamecombo_for_gamename( const char* gamename ){
130         if ( ( strlen( gamename ) == 0 ) || !strcmp( gamename, globalGameComboConfiguration().basegame ) ) {
131                 return gamecombo_t( 0, "", false );
132         }
133         else if ( !strcmp( gamename, globalGameComboConfiguration().known ) ) {
134                 return gamecombo_t( 1, globalGameComboConfiguration().known_dir, false );
135         }
136         else
137         {
138                 return gamecombo_t( string_empty( globalGameComboConfiguration().known_dir ) ? 1 : 2, "", true );
139         }
140 }
141
142 inline void path_copy_clean( char* destination, const char* source ){
143         char* i = destination;
144
145         while ( *source != '\0' )
146         {
147                 *i++ = ( *source == '\\' ) ? '/' : *source;
148                 ++source;
149         }
150
151         if ( i != destination && *( i - 1 ) != '/' ) {
152                 *( i++ ) = '/';
153         }
154
155         *i = '\0';
156 }
157
158
159 struct GameCombo
160 {
161         ui::ComboBoxText game_select{ui::null};
162         ui::Entry fsgame_entry{ui::null};
163 };
164
165 gboolean OnSelchangeComboWhatgame( ui::Widget widget, GameCombo* combo ){
166         const char *gamename;
167         {
168                 GtkTreeIter iter;
169                 gtk_combo_box_get_active_iter( combo->game_select, &iter );
170                 gtk_tree_model_get( gtk_combo_box_get_model( combo->game_select ), &iter, 0, (gpointer*)&gamename, -1 );
171         }
172
173         gamecombo_t gamecombo = gamecombo_for_gamename( gamename );
174
175         combo->fsgame_entry.text( gamecombo.fs_game );
176         gtk_widget_set_sensitive( combo->fsgame_entry , gamecombo.sensitive );
177
178         return FALSE;
179 }
180
181 class MappingMode
182 {
183 public:
184 bool do_mapping_mode;
185 const char* sp_mapping_mode;
186 const char* mp_mapping_mode;
187
188 MappingMode() :
189         do_mapping_mode( !string_empty( g_pGameDescription->getKeyValue( "show_gamemode" ) ) ),
190         sp_mapping_mode( "Single Player mapping mode" ),
191         mp_mapping_mode( "Multiplayer mapping mode" ){
192 }
193 };
194
195 typedef LazyStatic<MappingMode> LazyStaticMappingMode;
196
197 inline MappingMode& globalMappingMode(){
198         return LazyStaticMappingMode::instance();
199 }
200
201 class ProjectSettingsDialog
202 {
203 public:
204 GameCombo game_combo;
205 ui::ComboBox gamemode_combo{ui::null};
206 };
207
208 ui::Window ProjectSettingsDialog_construct( ProjectSettingsDialog& dialog, ModalDialog& modal ){
209         auto window = MainFrame_getWindow().create_dialog_window("Project Settings", G_CALLBACK(dialog_delete_callback ), &modal );
210
211         {
212                 auto table1 = create_dialog_table( 1, 2, 4, 4, 4 );
213                 window.add(table1);
214                 {
215             auto vbox = create_dialog_vbox( 4 );
216             table1.attach(vbox, {1, 2, 0, 1}, {GTK_FILL, GTK_FILL});
217                         {
218                 auto button = create_dialog_button( "OK", G_CALLBACK( dialog_button_ok ), &modal );
219                                 vbox.pack_start( button, FALSE, FALSE, 0 );
220                         }
221                         {
222                 auto button = create_dialog_button( "Cancel", G_CALLBACK( dialog_button_cancel ), &modal );
223                                 vbox.pack_start( button, FALSE, FALSE, 0 );
224                         }
225                 }
226                 {
227                         auto frame = create_dialog_frame( "Project settings" );
228             table1.attach(frame, {0, 1, 0, 1}, {GTK_EXPAND | GTK_FILL, GTK_FILL});
229                         {
230                                 auto table2 = create_dialog_table( ( globalMappingMode().do_mapping_mode ) ? 4 : 3, 2, 4, 4, 4 );
231                                 frame.add(table2);
232
233                                 {
234                                         auto label = ui::Label( "Select mod" );
235                                         label.show();
236                     table2.attach(label, {0, 1, 0, 1}, {GTK_FILL, 0});
237                                         gtk_misc_set_alignment( GTK_MISC( label ), 1, 0.5 );
238                                 }
239                                 {
240                                         dialog.game_combo.game_select = ui::ComboBoxText(ui::New);
241
242                                         gtk_combo_box_text_append_text( dialog.game_combo.game_select, globalGameComboConfiguration().basegame );
243                                         if ( globalGameComboConfiguration().known[0] != '\0' ) {
244                                                 gtk_combo_box_text_append_text( dialog.game_combo.game_select, globalGameComboConfiguration().known );
245                                         }
246                                         gtk_combo_box_text_append_text( dialog.game_combo.game_select, globalGameComboConfiguration().custom );
247
248                                         dialog.game_combo.game_select.show();
249                     table2.attach(dialog.game_combo.game_select, {1, 2, 0, 1}, {GTK_EXPAND | GTK_FILL, 0});
250
251                                         dialog.game_combo.game_select.connect( "changed", G_CALLBACK( OnSelchangeComboWhatgame ), &dialog.game_combo );
252                                 }
253
254                                 {
255                                         auto label = ui::Label( "fs_game" );
256                                         label.show();
257                     table2.attach(label, {0, 1, 1, 2}, {GTK_FILL, 0});
258                                         gtk_misc_set_alignment( GTK_MISC( label ), 1, 0.5 );
259                                 }
260                                 {
261                                         auto entry = ui::Entry(ui::New);
262                                         entry.show();
263                     table2.attach(entry, {1, 2, 1, 2}, {GTK_EXPAND | GTK_FILL, 0});
264
265                                         dialog.game_combo.fsgame_entry = entry;
266                                 }
267
268                                 if ( globalMappingMode().do_mapping_mode ) {
269                                         auto label = ui::Label( "Mapping mode" );
270                                         label.show();
271                     table2.attach(label, {0, 1, 3, 4}, {GTK_FILL, 0});
272                                         gtk_misc_set_alignment( GTK_MISC( label ), 1, 0.5 );
273
274                                         auto combo = ui::ComboBoxText(ui::New);
275                                         gtk_combo_box_text_append_text( combo, globalMappingMode().sp_mapping_mode );
276                                         gtk_combo_box_text_append_text( combo, globalMappingMode().mp_mapping_mode );
277
278                                         combo.show();
279                     table2.attach(combo, {1, 2, 3, 4}, {GTK_EXPAND | GTK_FILL, 0});
280
281                                         dialog.gamemode_combo = combo;
282                                 }
283                         }
284                 }
285         }
286
287         // initialise the fs_game selection from the project settings into the dialog
288         const char* dir = gamename_get();
289         gamecombo_t gamecombo = gamecombo_for_dir( dir );
290
291         gtk_combo_box_set_active( dialog.game_combo.game_select, gamecombo.game );
292         dialog.game_combo.fsgame_entry.text( gamecombo.fs_game );
293         gtk_widget_set_sensitive( dialog.game_combo.fsgame_entry , gamecombo.sensitive );
294
295         if ( globalMappingMode().do_mapping_mode ) {
296                 const char *gamemode = gamemode_get();
297                 if ( string_empty( gamemode ) || string_equal( gamemode, "sp" ) ) {
298                         gtk_combo_box_set_active( dialog.gamemode_combo, 0 );
299                 }
300                 else
301                 {
302                         gtk_combo_box_set_active( dialog.gamemode_combo, 1 );
303                 }
304         }
305
306         return window;
307 }
308
309 void ProjectSettingsDialog_ok( ProjectSettingsDialog& dialog ){
310         const char* dir = gtk_entry_get_text( dialog.game_combo.fsgame_entry );
311
312         const char* new_gamename = path_equal( dir, globalGameComboConfiguration().basegame_dir )
313                                                            ? ""
314                                                            : dir;
315
316         if ( !path_equal( new_gamename, gamename_get() ) ) {
317                 ScopeDisableScreenUpdates disableScreenUpdates( "Processing...", "Changing Game Name" );
318
319                 EnginePath_Unrealise();
320
321                 gamename_set( new_gamename );
322
323                 EnginePath_Realise();
324         }
325
326         if ( globalMappingMode().do_mapping_mode ) {
327                 // read from gamemode_combo
328                 int active = gtk_combo_box_get_active( dialog.gamemode_combo );
329                 if ( active == -1 || active == 0 ) {
330                         gamemode_set( "sp" );
331                 }
332                 else
333                 {
334                         gamemode_set( "mp" );
335                 }
336         }
337 }
338
339 void DoProjectSettings(){
340         //if ( ConfirmModified( "Edit Project Settings" ) ) {
341                 ModalDialog modal;
342                 ProjectSettingsDialog dialog;
343
344                 ui::Window window = ProjectSettingsDialog_construct( dialog, modal );
345
346                 if ( modal_dialog_show( window, modal ) == eIDOK ) {
347                         ProjectSettingsDialog_ok( dialog );
348                 }
349
350                 window.destroy();
351         //}
352 }
353
354 // =============================================================================
355 // Arbitrary Sides dialog
356
357 void DoSides( int type, int axis ){
358         ModalDialog dialog;
359         //GtkEntry* sides_entry;
360         GtkWidget* sides_spin;
361
362         auto window = MainFrame_getWindow().create_dialog_window("Arbitrary sides", G_CALLBACK(dialog_delete_callback ), &dialog );
363
364         auto accel = ui::AccelGroup(ui::New);
365         window.add_accel_group( accel );
366
367         auto sides_entry = ui::Entry(ui::New);
368         {
369                 auto hbox = create_dialog_hbox( 4, 4 );
370                 window.add(hbox);
371                 {
372                         auto label = ui::Label( "Sides:" );
373                         label.show();
374                         hbox.pack_start( label, FALSE, FALSE, 0 );
375                 }
376 //              {
377 //                      auto entry = sides_entry;
378 //                      entry.show();
379 //                      hbox.pack_start( entry, FALSE, FALSE, 0 );
380 //                      gtk_widget_grab_focus( entry  );
381 //              }
382                 {
383                         GtkAdjustment* adj;
384                         EBrushPrefab BrushPrefabType = (EBrushPrefab)type;
385                         switch ( BrushPrefabType )
386                         {
387                         case eBrushPrism :
388                         case eBrushCone :
389                                 adj = GTK_ADJUSTMENT( gtk_adjustment_new( 8, 3, 1022, 1, 10, 0 ) );
390                                 break;
391                         case eBrushSphere :
392                                 adj = GTK_ADJUSTMENT( gtk_adjustment_new( 8, 3, 31, 1, 10, 0 ) );
393                                 break;
394                         case eBrushRock :
395                                 adj = GTK_ADJUSTMENT( gtk_adjustment_new( 32, 10, 1000, 1, 10, 0 ) );
396                                 break;
397                         default:
398                                 adj = GTK_ADJUSTMENT( gtk_adjustment_new( 8, 3, 31, 1, 10, 0 ) );
399                                 break;
400                         }
401
402                         GtkWidget* spin = gtk_spin_button_new( adj, 1, 0 );
403                         gtk_widget_show( spin );
404                         gtk_box_pack_start( GTK_BOX( hbox ), spin, FALSE, FALSE, 0 );
405                         gtk_widget_set_size_request( spin, 64, -1 );
406                         gtk_spin_button_set_numeric( GTK_SPIN_BUTTON( spin ), TRUE );
407
408                         sides_spin = spin;
409                 }
410                 {
411             auto vbox = create_dialog_vbox( 4 );
412                         hbox.pack_start( vbox, TRUE, TRUE, 0 );
413                         {
414                                 auto button = create_dialog_button( "OK", G_CALLBACK( dialog_button_ok ), &dialog );
415                                 vbox.pack_start( button, FALSE, FALSE, 0 );
416                                 widget_make_default( button );
417                                 gtk_widget_add_accelerator( button , "clicked", accel, GDK_KEY_Return, (GdkModifierType)0, (GtkAccelFlags)0 );
418                         }
419                         {
420                 auto button = create_dialog_button( "Cancel", G_CALLBACK( dialog_button_cancel ), &dialog );
421                                 vbox.pack_start( button, FALSE, FALSE, 0 );
422                                 gtk_widget_add_accelerator( button , "clicked", accel, GDK_KEY_Escape, (GdkModifierType)0, (GtkAccelFlags)0 );
423                         }
424                 }
425         }
426
427         if ( modal_dialog_show( window, dialog ) == eIDOK ) {
428 //              const char *str = gtk_entry_get_text( sides_entry );
429
430 //              Scene_BrushConstructPrefab( GlobalSceneGraph(), (EBrushPrefab)type, atoi( str ), TextureBrowser_GetSelectedShader( GlobalTextureBrowser() ) );
431                 gtk_spin_button_update ( GTK_SPIN_BUTTON( sides_spin ) );
432                 int sides = static_cast<int>( gtk_spin_button_get_value( GTK_SPIN_BUTTON( sides_spin ) ) );
433                 Scene_BrushConstructPrefab( GlobalSceneGraph(), (EBrushPrefab)type, sides, TextureBrowser_GetSelectedShader( GlobalTextureBrowser() ) );
434         }
435
436         window.destroy();
437 }
438
439 // =============================================================================
440 // About dialog (no program is complete without one)
441
442 void about_button_changelog( ui::Widget widget, gpointer data ){
443         StringOutputStream log( 256 );
444         log << "https://gitlab.com/xonotic/netradiant/commits/master";
445         OpenURL( log.c_str() );
446 }
447
448 void about_button_credits( ui::Widget widget, gpointer data ){
449         StringOutputStream cred( 256 );
450         cred << "https://gitlab.com/xonotic/netradiant/graphs/master";
451         OpenURL( cred.c_str() );
452 }
453
454 void about_button_issues( ui::Widget widget, gpointer data ){
455         StringOutputStream cred( 256 );
456         cred << "https://gitlab.com/xonotic/netradiant/issues";
457         OpenURL( cred.c_str() );
458 }
459
460 static void AddParagraph( ui::VBox vbox, const char* text, bool use_markup ){
461         auto label = ui::Label( text );
462         gtk_label_set_use_markup( GTK_LABEL( label ), use_markup );
463         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0 );
464         gtk_label_set_justify( label, GTK_JUSTIFY_LEFT );
465         label.show();
466         vbox.pack_start( label, TRUE, TRUE, 0 );
467 }
468
469 static void AddParagraph( ui::VBox vbox, const char* text ){
470         AddParagraph( vbox, text, false );
471 }
472
473 void DoAbout(){
474         ModalDialog dialog;
475         ModalDialogButton ok_button( dialog, eIDOK );
476
477         auto window = MainFrame_getWindow().create_modal_dialog_window("About " RADIANT_NAME, dialog );
478
479         {
480                 auto vbox = create_dialog_vbox( 4, 4 );
481                 window.add(vbox);
482
483                 {
484             auto hbox = create_dialog_hbox( 4 );
485                         vbox.pack_start( hbox, FALSE, FALSE, 0 );
486
487                         {
488                 auto vbox2 = create_dialog_vbox( 4 );
489                                 hbox.pack_start( vbox2, FALSE, FALSE, 5 );
490                                 {
491                                         auto frame = create_dialog_frame( 0, ui::Shadow::IN );
492                                         vbox2.pack_start( frame, FALSE, FALSE, 0 );
493                                         {
494                                                 auto image = new_local_image( "logo.png" );
495                                                 image.show();
496                                                 frame.add(image);
497                                         }
498                                 }
499                         }
500
501                         {
502                                 // HACK: that may not be related to font size
503                                 auto about_vbox = ui::VBox( FALSE, 5 );
504                                 about_vbox.show();
505                                 hbox.pack_start( about_vbox, FALSE, FALSE, 0 );
506
507                                 AddParagraph( about_vbox,
508                                         RADIANT_NAME " " RADIANT_VERSION_STRING " (" __DATE__ ")\n"
509                                         RADIANT_ABOUTMSG );
510                                 AddParagraph( about_vbox,
511                                         "Get news and latest build at "
512                                         "<a href='https://netradiant.gitlab.io/'>"
513                                                 "netradiant.gitlab.io"
514                                         "</a>\n"
515                                         "Please report your issues at "
516                                         "<a href='https://gitlab.com/xonotic/netradiant/issues'>"
517                                                 "gitlab.com/xonotic/netradiant/issues"
518                                         "</a>\n"
519                                         "The team cannot provide support for custom builds.", true );
520                                 AddParagraph( about_vbox,
521                                         RADIANT_NAME " is a community project maintained by "
522                                         "<a href='https://xonotic.org'>"
523                                                 "Xonotic"
524                                         "</a>\n"
525                                         "and developed with help from "
526                                         "<a href='https://netradiant.gitlab.io/page/about/'>"
527                                                 "other game projects"
528                                         "</a> and individuals. ", true );
529                                 AddParagraph( about_vbox,
530                                         "This program is free software licensed under the GNU GPL." );
531                         }
532
533                         {
534                 auto vbox2 = create_dialog_vbox( 4 );
535                                 hbox.pack_start( vbox2, TRUE, TRUE, 0 );
536                                 {
537                     auto button = create_modal_dialog_button( "OK", ok_button );
538                                         vbox2.pack_start( button, FALSE, FALSE, 0 );
539                                         gtk_widget_grab_focus( GTK_WIDGET( button ) );
540                                 }
541                                 {
542                     auto button = create_dialog_button( "Credits", G_CALLBACK( about_button_credits ), 0 );
543                                         vbox2.pack_start( button, FALSE, FALSE, 0 );
544                                         gtk_widget_set_sensitive( GTK_WIDGET( button ), FALSE);
545                                 }
546                                 {
547                     auto button = create_dialog_button( "Changes", G_CALLBACK( about_button_changelog ), 0 );
548                                         vbox2.pack_start( button, FALSE, FALSE, 0 );
549                                 }
550                                 {
551                     auto button = create_dialog_button( "Issues", G_CALLBACK( about_button_issues ), 0 );
552                                         vbox2.pack_start( button, FALSE, FALSE, 0 );
553                                         gtk_widget_set_sensitive( GTK_WIDGET( button ), FALSE);
554                                 }
555                         }
556                 }
557                 {
558                         auto frame = create_dialog_frame( "OpenGL Properties" );
559                         vbox.pack_start( frame, FALSE, FALSE, 0 );
560                         {
561                                 auto table = create_dialog_table( 3, 2, 4, 4, 4 );
562                                 frame.add(table);
563                                 {
564                                         auto label = ui::Label( "Vendor:" );
565                                         label.show();
566                     table.attach(label, {0, 1, 0, 1}, {GTK_FILL, 0});
567                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
568                                 }
569                                 {
570                                         auto label = ui::Label( "Version:" );
571                                         label.show();
572                     table.attach(label, {0, 1, 1, 2}, {GTK_FILL, 0});
573                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
574                                 }
575                                 {
576                                         auto label = ui::Label( "Renderer:" );
577                                         label.show();
578                     table.attach(label, {0, 1, 2, 3}, {GTK_FILL, 0});
579                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
580                                 }
581                                 {
582                                         auto label = ui::Label( reinterpret_cast<const char*>( glGetString( GL_VENDOR ) ) );
583                                         label.show();
584                     table.attach(label, {1, 2, 0, 1}, {GTK_FILL, 0});
585                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
586                                 }
587                                 {
588                                         auto label = ui::Label( reinterpret_cast<const char*>( glGetString( GL_VERSION ) ) );
589                                         label.show();
590                     table.attach(label, {1, 2, 1, 2}, {GTK_FILL, 0});
591                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
592                                 }
593                                 {
594                                         auto label = ui::Label( reinterpret_cast<const char*>( glGetString( GL_RENDERER ) ) );
595                                         label.show();
596                     table.attach(label, {1, 2, 2, 3}, {GTK_FILL, 0});
597                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
598                                 }
599                         }
600                         {
601                                 auto frame = create_dialog_frame( "OpenGL Extensions" );
602                                 vbox.pack_start( frame, TRUE, TRUE, 0 );
603                                 {
604                                         auto sc_extensions = create_scrolled_window( ui::Policy::AUTOMATIC, ui::Policy::ALWAYS, 4 );
605                                         frame.add(sc_extensions);
606                                         {
607                                                 auto text_extensions = ui::TextView(ui::New);
608                                                 gtk_text_view_set_editable( text_extensions, FALSE );
609                                                 sc_extensions.add(text_extensions);
610                                                 text_extensions.text(reinterpret_cast<const char *>(glGetString(GL_EXTENSIONS)));
611                                                 gtk_text_view_set_wrap_mode( text_extensions, GTK_WRAP_WORD );
612                                                 text_extensions.show();
613                                         }
614                                 }
615                         }
616                 }
617         }
618
619         modal_dialog_show( window, dialog );
620
621         window.destroy();
622 }
623
624 // =============================================================================
625 // TextureLayout dialog
626
627 // Last used texture scale values
628 static float last_used_texture_layout_scale_x = 4.0;
629 static float last_used_texture_layout_scale_y = 4.0;
630
631 EMessageBoxReturn DoTextureLayout( float *fx, float *fy ){
632         ModalDialog dialog;
633         ModalDialogButton ok_button( dialog, eIDOK );
634         ModalDialogButton cancel_button( dialog, eIDCANCEL );
635         ui::Entry x{ui::null};
636         ui::Entry y{ui::null};
637
638         auto window = MainFrame_getWindow().create_modal_dialog_window("Patch texture layout", dialog );
639
640         auto accel = ui::AccelGroup(ui::New);
641         window.add_accel_group( accel );
642
643         {
644                 auto hbox = create_dialog_hbox( 4, 4 );
645                 window.add(hbox);
646                 {
647             auto vbox = create_dialog_vbox( 4 );
648                         hbox.pack_start( vbox, TRUE, TRUE, 0 );
649                         {
650                                 auto label = ui::Label( "Texture will be fit across the patch based\n"
651                                                                                                                         "on the x and y values given. Values of 1x1\n"
652                                                                                                                         "will \"fit\" the texture. 2x2 will repeat\n"
653                                                                                                                         "it twice, etc." );
654                                 label.show();
655                                 vbox.pack_start( label, TRUE, TRUE, 0 );
656                                 gtk_label_set_justify( label, GTK_JUSTIFY_LEFT );
657                         }
658                         {
659                                 auto table = create_dialog_table( 2, 2, 4, 4 );
660                                 table.show();
661                                 vbox.pack_start( table, TRUE, TRUE, 0 );
662                                 {
663                                         auto label = ui::Label( "Texture x:" );
664                                         label.show();
665                     table.attach(label, {0, 1, 0, 1}, {GTK_FILL, 0});
666                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
667                                 }
668                                 {
669                                         auto label = ui::Label( "Texture y:" );
670                                         label.show();
671                     table.attach(label, {0, 1, 1, 2}, {GTK_FILL, 0});
672                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
673                                 }
674                                 {
675                                         auto entry = ui::Entry(ui::New);
676                                         entry.show();
677                     table.attach(entry, {1, 2, 0, 1}, {GTK_EXPAND | GTK_FILL, 0});
678
679                                         x = entry;
680                                 }
681                                 {
682                                         auto entry = ui::Entry(ui::New);
683                                         entry.show();
684                     table.attach(entry, {1, 2, 1, 2}, {GTK_EXPAND | GTK_FILL, 0});
685
686                                         y = entry;
687                                 }
688                         }
689                 }
690                 {
691             auto vbox = create_dialog_vbox( 4 );
692                         hbox.pack_start( vbox, FALSE, FALSE, 0 );
693                         {
694                                 auto button = create_modal_dialog_button( "OK", ok_button );
695                                 vbox.pack_start( button, FALSE, FALSE, 0 );
696                                 widget_make_default( button );
697                                 gtk_widget_add_accelerator( button , "clicked", accel, GDK_KEY_Return, (GdkModifierType)0, (GtkAccelFlags)0 );
698                         }
699                         {
700                 auto button = create_modal_dialog_button( "Cancel", cancel_button );
701                                 vbox.pack_start( button, FALSE, FALSE, 0 );
702                                 gtk_widget_add_accelerator( button , "clicked", accel, GDK_KEY_Escape, (GdkModifierType)0, (GtkAccelFlags)0 );
703                         }
704                 }
705         }
706
707         // Initialize with last used values
708         char buf[16];
709
710         sprintf( buf, "%f", last_used_texture_layout_scale_x );
711         x.text( buf );
712
713         sprintf( buf, "%f", last_used_texture_layout_scale_y );
714         y.text( buf );
715
716         // Set focus after intializing the values
717         gtk_widget_grab_focus( x  );
718
719         EMessageBoxReturn ret = modal_dialog_show( window, dialog );
720         if ( ret == eIDOK ) {
721                 *fx = static_cast<float>( atof( gtk_entry_get_text( x ) ) );
722                 *fy = static_cast<float>( atof( gtk_entry_get_text( y ) ) );
723
724                 // Remember last used values
725                 last_used_texture_layout_scale_x = *fx;
726                 last_used_texture_layout_scale_y = *fy;
727         }
728
729         window.destroy();
730
731         return ret;
732 }
733
734 // =============================================================================
735 // Text Editor dialog
736
737 // master window widget
738 static ui::Window text_editor{ui::null};
739 static ui::Widget text_widget{ui::null}; // slave, text widget from the gtk editor
740 static GtkTextBuffer* text_buffer_;
741
742 static gint editor_delete( ui::Widget widget, gpointer data ){
743 /*      if ( ui::alert( widget.window(), "Close the shader editor ?", "Radiant", ui::alert_type::YESNO, ui::alert_icon::Question ) == ui::alert_response::NO ) {
744                 return TRUE;
745         }
746 */
747         text_editor.hide();
748
749         return TRUE;
750 }
751
752 static void editor_save( ui::Widget widget, gpointer data ){
753         FILE *f = fopen( (char*)g_object_get_data( G_OBJECT( data ), "filename" ), "w" );
754         //gpointer text = g_object_get_data( G_OBJECT( data ), "text" );
755
756         if ( f == 0 ) {
757                 ui::alert( ui::Widget::from(data).window(), "Error saving file !" );
758                 return;
759         }
760
761         /* Obtain iters for the start and end of points of the buffer */
762         GtkTextIter start;
763         GtkTextIter end;
764         gtk_text_buffer_get_start_iter (text_buffer_, &start);
765         gtk_text_buffer_get_end_iter (text_buffer_, &end);
766
767         /* Get the entire buffer text. */
768         char *str = gtk_text_buffer_get_text (text_buffer_, &start, &end, FALSE);
769
770         //char *str = gtk_editable_get_chars( GTK_EDITABLE( text ), 0, -1 );
771         fwrite( str, 1, strlen( str ), f );
772         fclose( f );
773         g_free (str);
774 }
775
776 static void editor_close( ui::Widget widget, gpointer data ){
777 /*      if ( ui::alert( text_editor.window(), "Close the shader editor ?", "Radiant", ui::alert_type::YESNO, ui::alert_icon::Question ) == ui::alert_response::NO ) {
778                 return;
779         }
780 */
781         text_editor.hide();
782 }
783
784 static void CreateGtkTextEditor(){
785         auto dlg = ui::Window( ui::window_type::TOP );
786
787         dlg.connect( "delete_event",
788                                           G_CALLBACK( editor_delete ), 0 );
789         gtk_window_set_default_size( dlg, 400, 300 );
790
791         auto vbox = ui::VBox( FALSE, 5 );
792         vbox.show();
793         dlg.add(vbox);
794         gtk_container_set_border_width( GTK_CONTAINER( vbox ), 5 );
795
796         auto scr = ui::ScrolledWindow(ui::New);
797         scr.show();
798         vbox.pack_start( scr, TRUE, TRUE, 0 );
799         gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scr ), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC );
800         gtk_scrolled_window_set_shadow_type( GTK_SCROLLED_WINDOW( scr ), GTK_SHADOW_IN );
801
802         auto text = ui::TextView(ui::New);
803         scr.add(text);
804         text.show();
805         g_object_set_data( G_OBJECT( dlg ), "text", (gpointer) text );
806         gtk_text_view_set_editable( text, TRUE );
807
808         auto hbox = ui::HBox( FALSE, 5 );
809         hbox.show();
810         vbox.pack_start( hbox, FALSE, TRUE, 0 );
811
812         auto button = ui::Button( "Close" );
813         button.show();
814         hbox.pack_end(button, FALSE, FALSE, 0);
815         button.connect( "clicked",
816                                           G_CALLBACK( editor_close ), dlg );
817         button.dimensions(60, -1);
818
819         button = ui::Button( "Save" );
820         button.show();
821         hbox.pack_end(button, FALSE, FALSE, 0);
822         button.connect( "clicked",
823                                           G_CALLBACK( editor_save ), dlg );
824         button.dimensions(60, -1);
825
826         text_editor = dlg;
827         text_widget = text;
828 }
829
830 static void DoGtkTextEditor( const char* filename, guint cursorpos, int length ){
831         if ( !text_editor ) {
832                 CreateGtkTextEditor(); // build it the first time we need it
833
834         }
835         // Load file
836         FILE *f = fopen( filename, "r" );
837
838         if ( f == 0 ) {
839                 globalOutputStream() << "Unable to load file " << filename << " in shader editor.\n";
840                 text_editor.hide();
841         }
842         else
843         {
844                 fseek( f, 0, SEEK_END );
845                 int len = ftell( f );
846                 void *buf = malloc( len );
847                 void *old_filename;
848
849                 rewind( f );
850                 fread( buf, 1, len, f );
851
852                 gtk_window_set_title( text_editor, filename );
853
854                 auto text_buffer = gtk_text_view_get_buffer(ui::TextView::from(text_widget));
855                 gtk_text_buffer_set_text( text_buffer, (char*)buf, length );
856
857                 old_filename = g_object_get_data( G_OBJECT( text_editor ), "filename" );
858                 if ( old_filename ) {
859                         free( old_filename );
860                 }
861                 g_object_set_data( G_OBJECT( text_editor ), "filename", strdup( filename ) );
862
863                 // trying to show later
864                 text_editor.show();
865                 gtk_window_present( GTK_WINDOW( text_editor ) );
866
867 #if GDEF_OS_WINDOWS
868                 ui::process();
869 #endif
870
871                 // only move the cursor if it's not exceeding the size..
872                 // NOTE: this is erroneous, cursorpos is the offset in bytes, not in characters
873                 // len is the max size in bytes, not in characters either, but the character count is below that limit..
874                 // thinking .. the difference between character count and byte count would be only because of CR/LF?
875                 {
876                         GtkTextIter text_iter;
877                         // character offset, not byte offset
878                         gtk_text_buffer_get_iter_at_offset( text_buffer, &text_iter, cursorpos );
879                         gtk_text_buffer_place_cursor( text_buffer, &text_iter );
880                         gtk_text_view_scroll_to_iter( GTK_TEXT_VIEW( text_widget ), &text_iter, 0, TRUE, 0, 0);
881                 }
882
883 #if GDEF_OS_WINDOWS
884                 gtk_widget_queue_draw( text_widget );
885 #endif
886
887                 text_buffer_ = text_buffer;
888                 free( buf );
889                 fclose( f );
890         }
891 }
892
893 // =============================================================================
894 // Light Intensity dialog
895
896 EMessageBoxReturn DoLightIntensityDlg( int *intensity ){
897         ModalDialog dialog;
898         ui::Entry intensity_entry{ui::null};
899         ModalDialogButton ok_button( dialog, eIDOK );
900         ModalDialogButton cancel_button( dialog, eIDCANCEL );
901
902         ui::Window window = MainFrame_getWindow().create_modal_dialog_window("Light intensity", dialog, -1, -1 );
903
904         auto accel_group = ui::AccelGroup(ui::New);
905         window.add_accel_group( accel_group );
906
907         {
908                 auto hbox = create_dialog_hbox( 4, 4 );
909                 window.add(hbox);
910                 {
911             auto vbox = create_dialog_vbox( 4 );
912                         hbox.pack_start( vbox, TRUE, TRUE, 0 );
913                         {
914                                 auto label = ui::Label( "ESC for default, ENTER to validate" );
915                                 label.show();
916                                 vbox.pack_start( label, FALSE, FALSE, 0 );
917                         }
918                         {
919                                 auto entry = ui::Entry(ui::New);
920                                 entry.show();
921                                 vbox.pack_start( entry, TRUE, TRUE, 0 );
922
923                                 gtk_widget_grab_focus( entry  );
924
925                                 intensity_entry = entry;
926                         }
927                 }
928                 {
929             auto vbox = create_dialog_vbox( 4 );
930                         hbox.pack_start( vbox, FALSE, FALSE, 0 );
931
932                         {
933                                 auto button = create_modal_dialog_button( "OK", ok_button );
934                                 vbox.pack_start( button, FALSE, FALSE, 0 );
935                                 widget_make_default( button );
936                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Return, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
937                         }
938                         {
939                 auto button = create_modal_dialog_button( "Cancel", cancel_button );
940                                 vbox.pack_start( button, FALSE, FALSE, 0 );
941                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Escape, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
942                         }
943                 }
944         }
945
946         char buf[16];
947         sprintf( buf, "%d", *intensity );
948         intensity_entry.text(buf);
949
950         EMessageBoxReturn ret = modal_dialog_show( window, dialog );
951         if ( ret == eIDOK ) {
952                 *intensity = atoi( gtk_entry_get_text( intensity_entry ) );
953         }
954
955         window.destroy();
956
957         return ret;
958 }
959
960 // =============================================================================
961 // Add new shader tag dialog
962
963 EMessageBoxReturn DoShaderTagDlg( CopiedString* tag, const char* title ){
964         ModalDialog dialog;
965         ModalDialogButton ok_button( dialog, eIDOK );
966         ModalDialogButton cancel_button( dialog, eIDCANCEL );
967
968         auto window = MainFrame_getWindow().create_modal_dialog_window(title, dialog, -1, -1 );
969
970         auto accel_group = ui::AccelGroup(ui::New);
971         window.add_accel_group( accel_group );
972
973         auto textentry = ui::Entry(ui::New);
974         {
975                 auto hbox = create_dialog_hbox( 4, 4 );
976                 window.add(hbox);
977                 {
978             auto vbox = create_dialog_vbox( 4 );
979                         hbox.pack_start( vbox, TRUE, TRUE, 0 );
980                         {
981                                 //GtkLabel* label = GTK_LABEL(gtk_label_new("Enter one ore more tags separated by spaces"));
982                                 auto label = ui::Label( "ESC to cancel, ENTER to validate" );
983                                 label.show();
984                                 vbox.pack_start( label, FALSE, FALSE, 0 );
985                         }
986                         {
987                                 auto entry = textentry;
988                                 entry.show();
989                                 vbox.pack_start( entry, TRUE, TRUE, 0 );
990
991                                 gtk_widget_grab_focus( entry  );
992                         }
993                 }
994                 {
995             auto vbox = create_dialog_vbox( 4 );
996                         hbox.pack_start( vbox, FALSE, FALSE, 0 );
997
998                         {
999                                 auto button = create_modal_dialog_button( "OK", ok_button );
1000                                 vbox.pack_start( button, FALSE, FALSE, 0 );
1001                                 widget_make_default( button );
1002                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Return, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
1003                         }
1004                         {
1005                 auto button = create_modal_dialog_button( "Cancel", cancel_button );
1006                                 vbox.pack_start( button, FALSE, FALSE, 0 );
1007                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Escape, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
1008                         }
1009                 }
1010         }
1011
1012         EMessageBoxReturn ret = modal_dialog_show( window, dialog );
1013         if ( ret == eIDOK ) {
1014                 *tag = gtk_entry_get_text( textentry );
1015         }
1016
1017         window.destroy();
1018
1019         return ret;
1020 }
1021
1022 EMessageBoxReturn DoShaderInfoDlg( const char* name, const char* filename, const char* title ){
1023         ModalDialog dialog;
1024         ModalDialogButton ok_button( dialog, eIDOK );
1025
1026         auto window = MainFrame_getWindow().create_modal_dialog_window(title, dialog, -1, -1 );
1027
1028         auto accel_group = ui::AccelGroup(ui::New);
1029         window.add_accel_group( accel_group );
1030
1031         {
1032                 auto hbox = create_dialog_hbox( 4, 4 );
1033                 window.add(hbox);
1034                 {
1035             auto vbox = create_dialog_vbox( 4 );
1036                         hbox.pack_start( vbox, FALSE, FALSE, 0 );
1037                         {
1038                                 auto label = ui::Label( "The selected shader" );
1039                                 label.show();
1040                                 vbox.pack_start( label, FALSE, FALSE, 0 );
1041                         }
1042                         {
1043                                 auto label = ui::Label( name );
1044                                 label.show();
1045                                 vbox.pack_start( label, FALSE, FALSE, 0 );
1046                         }
1047                         {
1048                                 auto label = ui::Label( "is located in file" );
1049                                 label.show();
1050                                 vbox.pack_start( label, FALSE, FALSE, 0 );
1051                         }
1052                         {
1053                                 auto label = ui::Label( filename );
1054                                 label.show();
1055                                 vbox.pack_start( label, FALSE, FALSE, 0 );
1056                         }
1057                         {
1058                                 auto button = create_modal_dialog_button( "OK", ok_button );
1059                                 vbox.pack_start( button, FALSE, FALSE, 0 );
1060                                 widget_make_default( button );
1061                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Return, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
1062                         }
1063                 }
1064         }
1065
1066         EMessageBoxReturn ret = modal_dialog_show( window, dialog );
1067
1068         window.destroy();
1069
1070         return ret;
1071 }
1072
1073
1074
1075 #if GDEF_OS_WINDOWS
1076 #include <gdk/gdkwin32.h>
1077 #endif
1078
1079 #if GDEF_OS_WINDOWS
1080 // use the file associations to open files instead of builtin Gtk editor
1081 bool g_TextEditor_useWin32Editor = false;
1082 #else
1083 // custom shader editor
1084 bool g_TextEditor_useCustomEditor = false;
1085 CopiedString g_TextEditor_editorCommand( "" );
1086 #endif
1087
1088 void DoTextEditor( const char* filename, int cursorpos, int length ){
1089 #if GDEF_OS_WINDOWS
1090         if ( g_TextEditor_useWin32Editor ) {
1091                 StringOutputStream path( 256 );
1092                 StringOutputStream modpath( 256 );
1093                 const char* gamename = GlobalRadiant().getGameName();
1094                 const char* basegame = GlobalRadiant().getRequiredGameDescriptionKeyValue( "basegame" );
1095                 const char* enginePath = GlobalRadiant().getEnginePath();
1096                 path << enginePath << basegame << '/' << filename;
1097                 modpath << enginePath << gamename << '/' << filename;
1098                 if ( file_exists( modpath.c_str() ) ){
1099                         globalOutputStream() << "opening file '" << modpath.c_str() << "' (line " << cursorpos << " info ignored)\n";
1100                         ShellExecute( (HWND)GDK_WINDOW_HWND( gtk_widget_get_window( MainFrame_getWindow() ) ), "open", modpath.c_str(), 0, 0, SW_SHOW );
1101                 }
1102                 else if ( file_exists( path.c_str() ) ){
1103                         globalOutputStream() << "opening file '" << path.c_str() << "' (line " << cursorpos << " info ignored)\n";
1104                         ShellExecute( (HWND)GDK_WINDOW_HWND( gtk_widget_get_window( MainFrame_getWindow() ) ), "open", path.c_str(), 0, 0, SW_SHOW );
1105                 }
1106                 else{
1107                         globalOutputStream() << "Failed to open '" << filename << "'\nOne sits in .pk3 most likely!\n";
1108                 }
1109                 return;
1110         }
1111         else{
1112                 StringOutputStream path( 256 );
1113                 StringOutputStream modpath( 256 );
1114                 const char* gamename = GlobalRadiant().getGameName();
1115                 const char* basegame = GlobalRadiant().getRequiredGameDescriptionKeyValue( "basegame" );
1116                 const char* enginePath = GlobalRadiant().getEnginePath();
1117                 path << enginePath << basegame << '/' << filename;
1118                 modpath << enginePath << gamename << '/' << filename;
1119                 if ( file_exists( modpath.c_str() ) ){
1120                         globalOutputStream() << "opening file '" << modpath.c_str() << "' (line " << cursorpos << " info ignored)\n";
1121                         DoGtkTextEditor( modpath.c_str(), cursorpos, length );
1122                 }
1123                 else if ( file_exists( path.c_str() ) ){
1124                         globalOutputStream() << "opening file '" << path.c_str() << "' (line " << cursorpos << " info ignored)\n";
1125                         DoGtkTextEditor( path.c_str(), cursorpos, length );
1126                 }
1127                 else{
1128                         globalOutputStream() << "Failed to open '" << filename << "'\nOne sits in .pk3 most likely!\n";
1129                 }
1130                 return;
1131         }
1132 #else
1133         // check if a custom editor is set
1134         if ( g_TextEditor_useCustomEditor && !g_TextEditor_editorCommand.empty() ) {
1135                 StringOutputStream strEditCommand( 256 );
1136                 strEditCommand << g_TextEditor_editorCommand.c_str() << " \"" << filename << "\"";
1137
1138                 globalOutputStream() << "Launching: " << strEditCommand.c_str() << "\n";
1139                 // note: linux does not return false if the command failed so it will assume success
1140                 if ( Q_Exec( 0, const_cast<char*>( strEditCommand.c_str() ), 0, true, false ) == false ) {
1141                         globalOutputStream() << "Failed to execute " << strEditCommand.c_str() << ", using default\n";
1142                 }
1143                 else
1144                 {
1145                         // the command (appeared) to run successfully, no need to do anything more
1146                         return;
1147                 }
1148         }
1149
1150         DoGtkTextEditor( filename, cursorpos, length );
1151 #endif
1152 }