]> git.xonotic.org Git - xonotic/netradiant.git/blob - radiant/gtkdlgs.cpp
Merge commit '7d7436ec3dd43cbdb04ad41f0a2c92a905f26807' into garux-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 "version.h"
44 #include "aboutmsg.h"
45
46 #include "igl.h"
47 #include "iscenegraph.h"
48 #include "iselection.h"
49
50 #include <gdk/gdkkeysyms.h>
51 #include <uilib/uilib.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
360         auto window = MainFrame_getWindow().create_dialog_window("Arbitrary sides", G_CALLBACK(dialog_delete_callback ), &dialog );
361
362         auto accel = ui::AccelGroup(ui::New);
363         window.add_accel_group( accel );
364
365         auto sides_entry = ui::Entry(ui::New);
366         {
367                 auto hbox = create_dialog_hbox( 4, 4 );
368                 window.add(hbox);
369                 {
370                         auto label = ui::Label( "Sides:" );
371                         label.show();
372                         hbox.pack_start( label, FALSE, FALSE, 0 );
373                 }
374                 {
375                         auto entry = sides_entry;
376                         entry.show();
377                         hbox.pack_start( entry, FALSE, FALSE, 0 );
378                         gtk_widget_grab_focus( entry  );
379                 }
380                 {
381             auto vbox = create_dialog_vbox( 4 );
382                         hbox.pack_start( vbox, TRUE, TRUE, 0 );
383                         {
384                                 auto button = create_dialog_button( "OK", G_CALLBACK( dialog_button_ok ), &dialog );
385                                 vbox.pack_start( button, FALSE, FALSE, 0 );
386                                 widget_make_default( button );
387                                 gtk_widget_add_accelerator( button , "clicked", accel, GDK_KEY_Return, (GdkModifierType)0, (GtkAccelFlags)0 );
388                         }
389                         {
390                 auto button = create_dialog_button( "Cancel", G_CALLBACK( dialog_button_cancel ), &dialog );
391                                 vbox.pack_start( button, FALSE, FALSE, 0 );
392                                 gtk_widget_add_accelerator( button , "clicked", accel, GDK_KEY_Escape, (GdkModifierType)0, (GtkAccelFlags)0 );
393                         }
394                 }
395         }
396
397         if ( modal_dialog_show( window, dialog ) == eIDOK ) {
398                 const char *str = gtk_entry_get_text( sides_entry );
399
400                 Scene_BrushConstructPrefab( GlobalSceneGraph(), (EBrushPrefab)type, atoi( str ), TextureBrowser_GetSelectedShader( GlobalTextureBrowser() ) );
401         }
402
403         window.destroy();
404 }
405
406 // =============================================================================
407 // About dialog (no program is complete without one)
408
409 void about_button_changelog( ui::Widget widget, gpointer data ){
410         StringOutputStream log( 256 );
411         log << "https://gitlab.com/xonotic/netradiant/commits/master";
412         OpenURL( log.c_str() );
413 }
414
415 void about_button_credits( ui::Widget widget, gpointer data ){
416         StringOutputStream cred( 256 );
417         cred << "https://gitlab.com/xonotic/netradiant/graphs/master";
418         OpenURL( cred.c_str() );
419 }
420
421 void about_button_issues( ui::Widget widget, gpointer data ){
422         StringOutputStream cred( 256 );
423         cred << "https://gitlab.com/xonotic/netradiant/issues";
424         OpenURL( cred.c_str() );
425 }
426
427 void DoAbout(){
428         ModalDialog dialog;
429         ModalDialogButton ok_button( dialog, eIDOK );
430
431         auto window = MainFrame_getWindow().create_modal_dialog_window("About NetRadiant", dialog );
432
433         {
434                 auto vbox = create_dialog_vbox( 4, 4 );
435                 window.add(vbox);
436
437                 {
438             auto hbox = create_dialog_hbox( 4 );
439                         vbox.pack_start( hbox, FALSE, TRUE, 0 );
440
441                         {
442                 auto vbox2 = create_dialog_vbox( 4 );
443                                 hbox.pack_start( vbox2, TRUE, FALSE, 0 );
444                                 {
445                                         auto frame = create_dialog_frame( 0, ui::Shadow::IN );
446                                         vbox2.pack_start( frame, FALSE, FALSE, 0 );
447                                         {
448                                                 auto image = new_local_image( "logo.png" );
449                                                 image.show();
450                                                 frame.add(image);
451                                         }
452                                 }
453                         }
454
455                         {
456                                 char const *label_text = "NetRadiant " RADIANT_VERSION "\n"
457                                                                                 __DATE__ "\n\n"
458                                         RADIANT_ABOUTMSG "\n\n"
459                                                                                 "This program is free software\n"
460                                                                                 "licensed under the GNU GPL.\n\n"
461                                                                                 "NetRadiant is unsupported, however\n"
462                                                                                 "you may report your problems at\n"
463                                                                                 "https://gitlab.com/xonotic/netradiant/issues";
464
465                                 auto label = ui::Label( label_text );
466
467                                 label.show();
468                                 hbox.pack_start( label, FALSE, FALSE, 0 );
469                                 gtk_misc_set_alignment( GTK_MISC( label ), 1, 0.5 );
470                                 gtk_label_set_justify( label, GTK_JUSTIFY_LEFT );
471                         }
472
473                         {
474                 auto vbox2 = create_dialog_vbox( 4 );
475                                 hbox.pack_start( vbox2, FALSE, TRUE, 0 );
476                                 {
477                     auto button = create_modal_dialog_button( "OK", ok_button );
478                                         vbox2.pack_start( button, FALSE, FALSE, 0 );
479                                 }
480                                 {
481                     auto button = create_dialog_button( "Credits", G_CALLBACK( about_button_credits ), 0 );
482                                         vbox2.pack_start( button, FALSE, FALSE, 0 );
483                                 }
484                                 {
485                     auto button = create_dialog_button( "Changes", G_CALLBACK( about_button_changelog ), 0 );
486                                         vbox2.pack_start( button, FALSE, FALSE, 0 );
487                                 }
488                                 {
489                     auto button = create_dialog_button( "Issues", G_CALLBACK( about_button_issues ), 0 );
490                                         vbox2.pack_start( button, FALSE, FALSE, 0 );
491                                 }
492                         }
493                 }
494                 {
495                         auto frame = create_dialog_frame( "OpenGL Properties" );
496                         vbox.pack_start( frame, FALSE, FALSE, 0 );
497                         {
498                                 auto table = create_dialog_table( 3, 2, 4, 4, 4 );
499                                 frame.add(table);
500                                 {
501                                         auto label = ui::Label( "Vendor:" );
502                                         label.show();
503                     table.attach(label, {0, 1, 0, 1}, {GTK_FILL, 0});
504                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
505                                 }
506                                 {
507                                         auto label = ui::Label( "Version:" );
508                                         label.show();
509                     table.attach(label, {0, 1, 1, 2}, {GTK_FILL, 0});
510                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
511                                 }
512                                 {
513                                         auto label = ui::Label( "Renderer:" );
514                                         label.show();
515                     table.attach(label, {0, 1, 2, 3}, {GTK_FILL, 0});
516                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
517                                 }
518                                 {
519                                         auto label = ui::Label( reinterpret_cast<const char*>( glGetString( GL_VENDOR ) ) );
520                                         label.show();
521                     table.attach(label, {1, 2, 0, 1}, {GTK_FILL, 0});
522                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
523                                 }
524                                 {
525                                         auto label = ui::Label( reinterpret_cast<const char*>( glGetString( GL_VERSION ) ) );
526                                         label.show();
527                     table.attach(label, {1, 2, 1, 2}, {GTK_FILL, 0});
528                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
529                                 }
530                                 {
531                                         auto label = ui::Label( reinterpret_cast<const char*>( glGetString( GL_RENDERER ) ) );
532                                         label.show();
533                     table.attach(label, {1, 2, 2, 3}, {GTK_FILL, 0});
534                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
535                                 }
536                         }
537                         {
538                                 auto frame = create_dialog_frame( "OpenGL Extensions" );
539                                 vbox.pack_start( frame, TRUE, TRUE, 0 );
540                                 {
541                                         auto sc_extensions = create_scrolled_window( ui::Policy::AUTOMATIC, ui::Policy::ALWAYS, 4 );
542                                         frame.add(sc_extensions);
543                                         {
544                                                 auto text_extensions = ui::TextView(ui::New);
545                                                 gtk_text_view_set_editable( text_extensions, FALSE );
546                                                 sc_extensions.add(text_extensions);
547                                                 text_extensions.text(reinterpret_cast<const char *>(glGetString(GL_EXTENSIONS)));
548                                                 gtk_text_view_set_wrap_mode( text_extensions, GTK_WRAP_WORD );
549                                                 text_extensions.show();
550                                         }
551                                 }
552                         }
553                 }
554         }
555
556         modal_dialog_show( window, dialog );
557
558         window.destroy();
559 }
560
561 // =============================================================================
562 // TextureLayout dialog
563
564 // Last used texture scale values
565 static float last_used_texture_layout_scale_x = 4.0;
566 static float last_used_texture_layout_scale_y = 4.0;
567
568 EMessageBoxReturn DoTextureLayout( float *fx, float *fy ){
569         ModalDialog dialog;
570         ModalDialogButton ok_button( dialog, eIDOK );
571         ModalDialogButton cancel_button( dialog, eIDCANCEL );
572         ui::Entry x{ui::null};
573         ui::Entry y{ui::null};
574
575         auto window = MainFrame_getWindow().create_modal_dialog_window("Patch texture layout", dialog );
576
577         auto accel = ui::AccelGroup(ui::New);
578         window.add_accel_group( accel );
579
580         {
581                 auto hbox = create_dialog_hbox( 4, 4 );
582                 window.add(hbox);
583                 {
584             auto vbox = create_dialog_vbox( 4 );
585                         hbox.pack_start( vbox, TRUE, TRUE, 0 );
586                         {
587                                 auto label = ui::Label( "Texture will be fit across the patch based\n"
588                                                                                                                         "on the x and y values given. Values of 1x1\n"
589                                                                                                                         "will \"fit\" the texture. 2x2 will repeat\n"
590                                                                                                                         "it twice, etc." );
591                                 label.show();
592                                 vbox.pack_start( label, TRUE, TRUE, 0 );
593                                 gtk_label_set_justify( label, GTK_JUSTIFY_LEFT );
594                         }
595                         {
596                                 auto table = create_dialog_table( 2, 2, 4, 4 );
597                                 table.show();
598                                 vbox.pack_start( table, TRUE, TRUE, 0 );
599                                 {
600                                         auto label = ui::Label( "Texture x:" );
601                                         label.show();
602                     table.attach(label, {0, 1, 0, 1}, {GTK_FILL, 0});
603                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
604                                 }
605                                 {
606                                         auto label = ui::Label( "Texture y:" );
607                                         label.show();
608                     table.attach(label, {0, 1, 1, 2}, {GTK_FILL, 0});
609                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
610                                 }
611                                 {
612                                         auto entry = ui::Entry(ui::New);
613                                         entry.show();
614                     table.attach(entry, {1, 2, 0, 1}, {GTK_EXPAND | GTK_FILL, 0});
615
616                                         x = entry;
617                                 }
618                                 {
619                                         auto entry = ui::Entry(ui::New);
620                                         entry.show();
621                     table.attach(entry, {1, 2, 1, 2}, {GTK_EXPAND | GTK_FILL, 0});
622
623                                         y = entry;
624                                 }
625                         }
626                 }
627                 {
628             auto vbox = create_dialog_vbox( 4 );
629                         hbox.pack_start( vbox, FALSE, FALSE, 0 );
630                         {
631                                 auto button = create_modal_dialog_button( "OK", ok_button );
632                                 vbox.pack_start( button, FALSE, FALSE, 0 );
633                                 widget_make_default( button );
634                                 gtk_widget_add_accelerator( button , "clicked", accel, GDK_KEY_Return, (GdkModifierType)0, (GtkAccelFlags)0 );
635                         }
636                         {
637                 auto button = create_modal_dialog_button( "Cancel", cancel_button );
638                                 vbox.pack_start( button, FALSE, FALSE, 0 );
639                                 gtk_widget_add_accelerator( button , "clicked", accel, GDK_KEY_Escape, (GdkModifierType)0, (GtkAccelFlags)0 );
640                         }
641                 }
642         }
643
644         // Initialize with last used values
645         char buf[16];
646
647         sprintf( buf, "%f", last_used_texture_layout_scale_x );
648         x.text( buf );
649
650         sprintf( buf, "%f", last_used_texture_layout_scale_y );
651         y.text( buf );
652
653         // Set focus after intializing the values
654         gtk_widget_grab_focus( x  );
655
656         EMessageBoxReturn ret = modal_dialog_show( window, dialog );
657         if ( ret == eIDOK ) {
658                 *fx = static_cast<float>( atof( gtk_entry_get_text( x ) ) );
659                 *fy = static_cast<float>( atof( gtk_entry_get_text( y ) ) );
660
661                 // Remember last used values
662                 last_used_texture_layout_scale_x = *fx;
663                 last_used_texture_layout_scale_y = *fy;
664         }
665
666         window.destroy();
667
668         return ret;
669 }
670
671 // =============================================================================
672 // Text Editor dialog
673
674 // master window widget
675 static ui::Window text_editor{ui::null};
676 static ui::Widget text_widget{ui::null}; // slave, text widget from the gtk editor
677 static GtkTextBuffer* text_buffer_;
678
679 static gint editor_delete( ui::Widget widget, gpointer data ){
680 /*      if ( ui::alert( widget.window(), "Close the shader editor ?", "Radiant", ui::alert_type::YESNO, ui::alert_icon::Question ) == ui::alert_response::NO ) {
681                 return TRUE;
682         }
683 */
684         text_editor.hide();
685
686         return TRUE;
687 }
688
689 static void editor_save( ui::Widget widget, gpointer data ){
690         FILE *f = fopen( (char*)g_object_get_data( G_OBJECT( data ), "filename" ), "w" );
691         //gpointer text = g_object_get_data( G_OBJECT( data ), "text" );
692
693         if ( f == 0 ) {
694                 ui::alert( ui::Widget::from(data).window(), "Error saving file !" );
695                 return;
696         }
697
698         /* Obtain iters for the start and end of points of the buffer */
699         GtkTextIter start;
700         GtkTextIter end;
701         gtk_text_buffer_get_start_iter (text_buffer_, &start);
702         gtk_text_buffer_get_end_iter (text_buffer_, &end);
703
704         /* Get the entire buffer text. */
705         char *str = gtk_text_buffer_get_text (text_buffer_, &start, &end, FALSE);
706
707         //char *str = gtk_editable_get_chars( GTK_EDITABLE( text ), 0, -1 );
708         fwrite( str, 1, strlen( str ), f );
709         fclose( f );
710         g_free (str);
711 }
712
713 static void editor_close( ui::Widget widget, gpointer data ){
714 /*      if ( ui::alert( text_editor.window(), "Close the shader editor ?", "Radiant", ui::alert_type::YESNO, ui::alert_icon::Question ) == ui::alert_response::NO ) {
715                 return;
716         }
717 */
718         text_editor.hide();
719 }
720
721 static void CreateGtkTextEditor(){
722         auto dlg = ui::Window( ui::window_type::TOP );
723
724         dlg.connect( "delete_event",
725                                           G_CALLBACK( editor_delete ), 0 );
726         gtk_window_set_default_size( dlg, 400, 300 );
727
728         auto vbox = ui::VBox( FALSE, 5 );
729         vbox.show();
730         dlg.add(vbox);
731         gtk_container_set_border_width( GTK_CONTAINER( vbox ), 5 );
732
733         auto scr = ui::ScrolledWindow(ui::New);
734         scr.show();
735         vbox.pack_start( scr, TRUE, TRUE, 0 );
736         gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scr ), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC );
737         gtk_scrolled_window_set_shadow_type( GTK_SCROLLED_WINDOW( scr ), GTK_SHADOW_IN );
738
739         auto text = ui::TextView(ui::New);
740         scr.add(text);
741         text.show();
742         g_object_set_data( G_OBJECT( dlg ), "text", (gpointer) text );
743         gtk_text_view_set_editable( text, TRUE );
744
745         auto hbox = ui::HBox( FALSE, 5 );
746         hbox.show();
747         vbox.pack_start( hbox, FALSE, TRUE, 0 );
748
749         auto button = ui::Button( "Close" );
750         button.show();
751         hbox.pack_end(button, FALSE, FALSE, 0);
752         button.connect( "clicked",
753                                           G_CALLBACK( editor_close ), dlg );
754         button.dimensions(60, -1);
755
756         button = ui::Button( "Save" );
757         button.show();
758         hbox.pack_end(button, FALSE, FALSE, 0);
759         button.connect( "clicked",
760                                           G_CALLBACK( editor_save ), dlg );
761         button.dimensions(60, -1);
762
763         text_editor = dlg;
764         text_widget = text;
765 }
766
767 static void DoGtkTextEditor( const char* filename, guint cursorpos, int length ){
768         if ( !text_editor ) {
769                 CreateGtkTextEditor(); // build it the first time we need it
770
771         }
772         // Load file
773         FILE *f = fopen( filename, "r" );
774
775         if ( f == 0 ) {
776                 globalOutputStream() << "Unable to load file " << filename << " in shader editor.\n";
777                 text_editor.hide();
778         }
779         else
780         {
781                 fseek( f, 0, SEEK_END );
782                 int len = ftell( f );
783                 void *buf = malloc( len );
784                 void *old_filename;
785
786                 rewind( f );
787                 fread( buf, 1, len, f );
788
789                 gtk_window_set_title( text_editor, filename );
790
791                 auto text_buffer = gtk_text_view_get_buffer(ui::TextView::from(text_widget));
792                 gtk_text_buffer_set_text( text_buffer, (char*)buf, length );
793
794                 old_filename = g_object_get_data( G_OBJECT( text_editor ), "filename" );
795                 if ( old_filename ) {
796                         free( old_filename );
797                 }
798                 g_object_set_data( G_OBJECT( text_editor ), "filename", strdup( filename ) );
799
800                 // trying to show later
801                 text_editor.show();
802
803 #if GDEF_OS_WINDOWS
804                 ui::process();
805 #endif
806
807                 // only move the cursor if it's not exceeding the size..
808                 // NOTE: this is erroneous, cursorpos is the offset in bytes, not in characters
809                 // len is the max size in bytes, not in characters either, but the character count is below that limit..
810                 // thinking .. the difference between character count and byte count would be only because of CR/LF?
811                 {
812                         GtkTextIter text_iter;
813                         // character offset, not byte offset
814                         gtk_text_buffer_get_iter_at_offset( text_buffer, &text_iter, cursorpos );
815                         gtk_text_buffer_place_cursor( text_buffer, &text_iter );
816                         gtk_text_view_scroll_to_iter( GTK_TEXT_VIEW( text_widget ), &text_iter, 0, TRUE, 0, 0);
817                 }
818
819 #if GDEF_OS_WINDOWS
820                 gtk_widget_queue_draw( text_widget );
821 #endif
822
823                 text_buffer_ = text_buffer;
824                 free( buf );
825                 fclose( f );
826         }
827 }
828
829 // =============================================================================
830 // Light Intensity dialog
831
832 EMessageBoxReturn DoLightIntensityDlg( int *intensity ){
833         ModalDialog dialog;
834         ui::Entry intensity_entry{ui::null};
835         ModalDialogButton ok_button( dialog, eIDOK );
836         ModalDialogButton cancel_button( dialog, eIDCANCEL );
837
838         ui::Window window = MainFrame_getWindow().create_modal_dialog_window("Light intensity", dialog, -1, -1 );
839
840         auto accel_group = ui::AccelGroup(ui::New);
841         window.add_accel_group( accel_group );
842
843         {
844                 auto hbox = create_dialog_hbox( 4, 4 );
845                 window.add(hbox);
846                 {
847             auto vbox = create_dialog_vbox( 4 );
848                         hbox.pack_start( vbox, TRUE, TRUE, 0 );
849                         {
850                                 auto label = ui::Label( "ESC for default, ENTER to validate" );
851                                 label.show();
852                                 vbox.pack_start( label, FALSE, FALSE, 0 );
853                         }
854                         {
855                                 auto entry = ui::Entry(ui::New);
856                                 entry.show();
857                                 vbox.pack_start( entry, TRUE, TRUE, 0 );
858
859                                 gtk_widget_grab_focus( entry  );
860
861                                 intensity_entry = entry;
862                         }
863                 }
864                 {
865             auto vbox = create_dialog_vbox( 4 );
866                         hbox.pack_start( vbox, FALSE, FALSE, 0 );
867
868                         {
869                                 auto button = create_modal_dialog_button( "OK", ok_button );
870                                 vbox.pack_start( button, FALSE, FALSE, 0 );
871                                 widget_make_default( button );
872                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Return, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
873                         }
874                         {
875                 auto button = create_modal_dialog_button( "Cancel", cancel_button );
876                                 vbox.pack_start( button, FALSE, FALSE, 0 );
877                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Escape, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
878                         }
879                 }
880         }
881
882         char buf[16];
883         sprintf( buf, "%d", *intensity );
884         intensity_entry.text(buf);
885
886         EMessageBoxReturn ret = modal_dialog_show( window, dialog );
887         if ( ret == eIDOK ) {
888                 *intensity = atoi( gtk_entry_get_text( intensity_entry ) );
889         }
890
891         window.destroy();
892
893         return ret;
894 }
895
896 // =============================================================================
897 // Add new shader tag dialog
898
899 EMessageBoxReturn DoShaderTagDlg( CopiedString* tag, const char* title ){
900         ModalDialog dialog;
901         ModalDialogButton ok_button( dialog, eIDOK );
902         ModalDialogButton cancel_button( dialog, eIDCANCEL );
903
904         auto window = MainFrame_getWindow().create_modal_dialog_window(title, dialog, -1, -1 );
905
906         auto accel_group = ui::AccelGroup(ui::New);
907         window.add_accel_group( accel_group );
908
909         auto textentry = ui::Entry(ui::New);
910         {
911                 auto hbox = create_dialog_hbox( 4, 4 );
912                 window.add(hbox);
913                 {
914             auto vbox = create_dialog_vbox( 4 );
915                         hbox.pack_start( vbox, TRUE, TRUE, 0 );
916                         {
917                                 //GtkLabel* label = GTK_LABEL(gtk_label_new("Enter one ore more tags separated by spaces"));
918                                 auto label = ui::Label( "ESC to cancel, ENTER to validate" );
919                                 label.show();
920                                 vbox.pack_start( label, FALSE, FALSE, 0 );
921                         }
922                         {
923                                 auto entry = textentry;
924                                 entry.show();
925                                 vbox.pack_start( entry, TRUE, TRUE, 0 );
926
927                                 gtk_widget_grab_focus( entry  );
928                         }
929                 }
930                 {
931             auto vbox = create_dialog_vbox( 4 );
932                         hbox.pack_start( vbox, FALSE, FALSE, 0 );
933
934                         {
935                                 auto button = create_modal_dialog_button( "OK", ok_button );
936                                 vbox.pack_start( button, FALSE, FALSE, 0 );
937                                 widget_make_default( button );
938                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Return, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
939                         }
940                         {
941                 auto button = create_modal_dialog_button( "Cancel", cancel_button );
942                                 vbox.pack_start( button, FALSE, FALSE, 0 );
943                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Escape, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
944                         }
945                 }
946         }
947
948         EMessageBoxReturn ret = modal_dialog_show( window, dialog );
949         if ( ret == eIDOK ) {
950                 *tag = gtk_entry_get_text( textentry );
951         }
952
953         window.destroy();
954
955         return ret;
956 }
957
958 EMessageBoxReturn DoShaderInfoDlg( const char* name, const char* filename, const char* title ){
959         ModalDialog dialog;
960         ModalDialogButton ok_button( dialog, eIDOK );
961
962         auto window = MainFrame_getWindow().create_modal_dialog_window(title, dialog, -1, -1 );
963
964         auto accel_group = ui::AccelGroup(ui::New);
965         window.add_accel_group( accel_group );
966
967         {
968                 auto hbox = create_dialog_hbox( 4, 4 );
969                 window.add(hbox);
970                 {
971             auto vbox = create_dialog_vbox( 4 );
972                         hbox.pack_start( vbox, FALSE, FALSE, 0 );
973                         {
974                                 auto label = ui::Label( "The selected shader" );
975                                 label.show();
976                                 vbox.pack_start( label, FALSE, FALSE, 0 );
977                         }
978                         {
979                                 auto label = ui::Label( name );
980                                 label.show();
981                                 vbox.pack_start( label, FALSE, FALSE, 0 );
982                         }
983                         {
984                                 auto label = ui::Label( "is located in file" );
985                                 label.show();
986                                 vbox.pack_start( label, FALSE, FALSE, 0 );
987                         }
988                         {
989                                 auto label = ui::Label( filename );
990                                 label.show();
991                                 vbox.pack_start( label, FALSE, FALSE, 0 );
992                         }
993                         {
994                                 auto button = create_modal_dialog_button( "OK", ok_button );
995                                 vbox.pack_start( button, FALSE, FALSE, 0 );
996                                 widget_make_default( button );
997                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Return, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
998                         }
999                 }
1000         }
1001
1002         EMessageBoxReturn ret = modal_dialog_show( window, dialog );
1003
1004         window.destroy();
1005
1006         return ret;
1007 }
1008
1009
1010
1011 #if GDEF_OS_WINDOWS
1012 #include <gdk/gdkwin32.h>
1013 #endif
1014
1015 #if GDEF_OS_WINDOWS
1016 // use the file associations to open files instead of builtin Gtk editor
1017 bool g_TextEditor_useWin32Editor = false;
1018 #else
1019 // custom shader editor
1020 bool g_TextEditor_useCustomEditor = false;
1021 CopiedString g_TextEditor_editorCommand( "" );
1022 #endif
1023
1024 void DoTextEditor( const char* filename, int cursorpos, int length ){
1025 #if GDEF_OS_WINDOWS
1026         if ( g_TextEditor_useWin32Editor ) {
1027                 StringOutputStream path( 256 );
1028                 StringOutputStream modpath( 256 );
1029                 const char* gamename = GlobalRadiant().getGameName();
1030                 const char* basegame = GlobalRadiant().getRequiredGameDescriptionKeyValue( "basegame" );
1031                 const char* enginePath = GlobalRadiant().getEnginePath();
1032                 path << enginePath << basegame << '/' << filename;
1033                 modpath << enginePath << gamename << '/' << filename;
1034                 if ( file_exists( modpath.c_str() ) ){
1035                         globalOutputStream() << "opening file '" << modpath.c_str() << "' (line " << cursorpos << " info ignored)\n";
1036                         ShellExecute( (HWND)GDK_WINDOW_HWND( gtk_widget_get_window( MainFrame_getWindow() ) ), "open", modpath.c_str(), 0, 0, SW_SHOW );
1037                 }
1038                 else if ( file_exists( path.c_str() ) ){
1039                         globalOutputStream() << "opening file '" << path.c_str() << "' (line " << cursorpos << " info ignored)\n";
1040                         ShellExecute( (HWND)GDK_WINDOW_HWND( gtk_widget_get_window( MainFrame_getWindow() ) ), "open", path.c_str(), 0, 0, SW_SHOW );
1041                 }
1042                 else{
1043                         globalOutputStream() << "Failed to open '" << filename << "\n";
1044                 }
1045                 return;
1046         }
1047         else{
1048                 StringOutputStream path( 256 );
1049                 StringOutputStream modpath( 256 );
1050                 const char* gamename = GlobalRadiant().getGameName();
1051                 const char* basegame = GlobalRadiant().getRequiredGameDescriptionKeyValue( "basegame" );
1052                 const char* enginePath = GlobalRadiant().getEnginePath();
1053                 path << enginePath << basegame << '/' << filename;
1054                 modpath << enginePath << gamename << '/' << filename;
1055                 if ( file_exists( modpath.c_str() ) ){
1056                         globalOutputStream() << "opening file '" << modpath.c_str() << "' (line " << cursorpos << " info ignored)\n";
1057                         DoGtkTextEditor( modpath.c_str(), cursorpos, length );
1058                 }
1059                 else if ( file_exists( path.c_str() ) ){
1060                         globalOutputStream() << "opening file '" << path.c_str() << "' (line " << cursorpos << " info ignored)\n";
1061                         DoGtkTextEditor( path.c_str(), cursorpos, length );
1062                 }
1063                 else{
1064                         globalOutputStream() << "Failed to open '" << filename << "\n";
1065                 }
1066                 return;
1067         }
1068 #else
1069         // check if a custom editor is set
1070         if ( g_TextEditor_useCustomEditor && !g_TextEditor_editorCommand.empty() ) {
1071                 StringOutputStream strEditCommand( 256 );
1072                 strEditCommand << g_TextEditor_editorCommand.c_str() << " \"" << filename << "\"";
1073
1074                 globalOutputStream() << "Launching: " << strEditCommand.c_str() << "\n";
1075                 // note: linux does not return false if the command failed so it will assume success
1076                 if ( Q_Exec( 0, const_cast<char*>( strEditCommand.c_str() ), 0, true, false ) == false ) {
1077                         globalOutputStream() << "Failed to execute " << strEditCommand.c_str() << ", using default\n";
1078                 }
1079                 else
1080                 {
1081                         // the command (appeared) to run successfully, no need to do anything more
1082                         return;
1083                 }
1084         }
1085
1086         DoGtkTextEditor( filename, cursorpos, length );
1087 #endif
1088 }