db65bbc2193e89ded1d8415e91d6abf5ccba8ddd
[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
52 #include "os/path.h"
53 #include "math/aabb.h"
54 #include "container/array.h"
55 #include "generic/static.h"
56 #include "stream/stringstream.h"
57 #include "convert.h"
58 #include "gtkutil/messagebox.h"
59 #include "gtkutil/image.h"
60
61 #include "gtkmisc.h"
62 #include "brushmanip.h"
63 #include "build.h"
64 #include "qe3.h"
65 #include "texwindow.h"
66 #include "xywindow.h"
67 #include "mainframe.h"
68 #include "preferences.h"
69 #include "url.h"
70 #include "cmdlib.h"
71
72
73
74 // =============================================================================
75 // Project settings dialog
76
77 class GameComboConfiguration
78 {
79 public:
80 const char* basegame_dir;
81 const char* basegame;
82 const char* known_dir;
83 const char* known;
84 const char* custom;
85
86 GameComboConfiguration() :
87         basegame_dir( g_pGameDescription->getRequiredKeyValue( "basegame" ) ),
88         basegame( g_pGameDescription->getRequiredKeyValue( "basegamename" ) ),
89         known_dir( g_pGameDescription->getKeyValue( "knowngame" ) ),
90         known( g_pGameDescription->getKeyValue( "knowngamename" ) ),
91         custom( g_pGameDescription->getRequiredKeyValue( "unknowngamename" ) ){
92 }
93 };
94
95 typedef LazyStatic<GameComboConfiguration> LazyStaticGameComboConfiguration;
96
97 inline GameComboConfiguration& globalGameComboConfiguration(){
98         return LazyStaticGameComboConfiguration::instance();
99 }
100
101
102 struct gamecombo_t
103 {
104         gamecombo_t( int _game, const char* _fs_game, bool _sensitive )
105                 : game( _game ), fs_game( _fs_game ), sensitive( _sensitive )
106         {}
107         int game;
108         const char* fs_game;
109         bool sensitive;
110 };
111
112 gamecombo_t gamecombo_for_dir( const char* dir ){
113         if ( string_equal( dir, globalGameComboConfiguration().basegame_dir ) ) {
114                 return gamecombo_t( 0, "", false );
115         }
116         else if ( string_equal( dir, globalGameComboConfiguration().known_dir ) ) {
117                 return gamecombo_t( 1, dir, false );
118         }
119         else
120         {
121                 return gamecombo_t( string_empty( globalGameComboConfiguration().known_dir ) ? 1 : 2, dir, true );
122         }
123 }
124
125 gamecombo_t gamecombo_for_gamename( const char* gamename ){
126         if ( ( strlen( gamename ) == 0 ) || !strcmp( gamename, globalGameComboConfiguration().basegame ) ) {
127                 return gamecombo_t( 0, "", false );
128         }
129         else if ( !strcmp( gamename, globalGameComboConfiguration().known ) ) {
130                 return gamecombo_t( 1, globalGameComboConfiguration().known_dir, false );
131         }
132         else
133         {
134                 return gamecombo_t( string_empty( globalGameComboConfiguration().known_dir ) ? 1 : 2, "", true );
135         }
136 }
137
138 inline void path_copy_clean( char* destination, const char* source ){
139         char* i = destination;
140
141         while ( *source != '\0' )
142         {
143                 *i++ = ( *source == '\\' ) ? '/' : *source;
144                 ++source;
145         }
146
147         if ( i != destination && *( i - 1 ) != '/' ) {
148                 *( i++ ) = '/';
149         }
150
151         *i = '\0';
152 }
153
154
155 struct GameCombo
156 {
157         ui::ComboBoxText game_select{ui::null};
158         ui::Entry fsgame_entry{ui::null};
159 };
160
161 gboolean OnSelchangeComboWhatgame( ui::Widget widget, GameCombo* combo ){
162         const char *gamename;
163         {
164                 GtkTreeIter iter;
165                 gtk_combo_box_get_active_iter( combo->game_select, &iter );
166                 gtk_tree_model_get( gtk_combo_box_get_model( combo->game_select ), &iter, 0, (gpointer*)&gamename, -1 );
167         }
168
169         gamecombo_t gamecombo = gamecombo_for_gamename( gamename );
170
171         combo->fsgame_entry.text( gamecombo.fs_game );
172         gtk_widget_set_sensitive( combo->fsgame_entry , gamecombo.sensitive );
173
174         return FALSE;
175 }
176
177 class MappingMode
178 {
179 public:
180 bool do_mapping_mode;
181 const char* sp_mapping_mode;
182 const char* mp_mapping_mode;
183
184 MappingMode() :
185         do_mapping_mode( !string_empty( g_pGameDescription->getKeyValue( "show_gamemode" ) ) ),
186         sp_mapping_mode( "Single Player mapping mode" ),
187         mp_mapping_mode( "Multiplayer mapping mode" ){
188 }
189 };
190
191 typedef LazyStatic<MappingMode> LazyStaticMappingMode;
192
193 inline MappingMode& globalMappingMode(){
194         return LazyStaticMappingMode::instance();
195 }
196
197 class ProjectSettingsDialog
198 {
199 public:
200 GameCombo game_combo;
201 ui::ComboBox gamemode_combo{ui::null};
202 };
203
204 ui::Window ProjectSettingsDialog_construct( ProjectSettingsDialog& dialog, ModalDialog& modal ){
205         auto window = MainFrame_getWindow().create_dialog_window("Project Settings", G_CALLBACK(dialog_delete_callback ), &modal );
206
207         {
208                 auto table1 = create_dialog_table( 1, 2, 4, 4, 4 );
209                 window.add(table1);
210                 {
211             auto vbox = create_dialog_vbox( 4 );
212             table1.attach(vbox, {1, 2, 0, 1}, {GTK_FILL, GTK_FILL});
213                         {
214                 auto button = create_dialog_button( "OK", G_CALLBACK( dialog_button_ok ), &modal );
215                                 vbox.pack_start( button, FALSE, FALSE, 0 );
216                         }
217                         {
218                 auto button = create_dialog_button( "Cancel", G_CALLBACK( dialog_button_cancel ), &modal );
219                                 vbox.pack_start( button, FALSE, FALSE, 0 );
220                         }
221                 }
222                 {
223                         auto frame = create_dialog_frame( "Project settings" );
224             table1.attach(frame, {0, 1, 0, 1}, {GTK_EXPAND | GTK_FILL, GTK_FILL});
225                         {
226                                 auto table2 = create_dialog_table( ( globalMappingMode().do_mapping_mode ) ? 4 : 3, 2, 4, 4, 4 );
227                                 frame.add(table2);
228
229                                 {
230                                         auto label = ui::Label( "Select mod" );
231                                         label.show();
232                     table2.attach(label, {0, 1, 0, 1}, {GTK_FILL, 0});
233                                         gtk_misc_set_alignment( GTK_MISC( label ), 1, 0.5 );
234                                 }
235                                 {
236                                         dialog.game_combo.game_select = ui::ComboBoxText(ui::New);
237
238                                         gtk_combo_box_text_append_text( dialog.game_combo.game_select, globalGameComboConfiguration().basegame );
239                                         if ( globalGameComboConfiguration().known[0] != '\0' ) {
240                                                 gtk_combo_box_text_append_text( dialog.game_combo.game_select, globalGameComboConfiguration().known );
241                                         }
242                                         gtk_combo_box_text_append_text( dialog.game_combo.game_select, globalGameComboConfiguration().custom );
243
244                                         dialog.game_combo.game_select.show();
245                     table2.attach(dialog.game_combo.game_select, {1, 2, 0, 1}, {GTK_EXPAND | GTK_FILL, 0});
246
247                                         dialog.game_combo.game_select.connect( "changed", G_CALLBACK( OnSelchangeComboWhatgame ), &dialog.game_combo );
248                                 }
249
250                                 {
251                                         auto label = ui::Label( "fs_game" );
252                                         label.show();
253                     table2.attach(label, {0, 1, 1, 2}, {GTK_FILL, 0});
254                                         gtk_misc_set_alignment( GTK_MISC( label ), 1, 0.5 );
255                                 }
256                                 {
257                                         auto entry = ui::Entry(ui::New);
258                                         entry.show();
259                     table2.attach(entry, {1, 2, 1, 2}, {GTK_EXPAND | GTK_FILL, 0});
260
261                                         dialog.game_combo.fsgame_entry = entry;
262                                 }
263
264                                 if ( globalMappingMode().do_mapping_mode ) {
265                                         auto label = ui::Label( "Mapping mode" );
266                                         label.show();
267                     table2.attach(label, {0, 1, 3, 4}, {GTK_FILL, 0});
268                                         gtk_misc_set_alignment( GTK_MISC( label ), 1, 0.5 );
269
270                                         auto combo = ui::ComboBoxText(ui::New);
271                                         gtk_combo_box_text_append_text( combo, globalMappingMode().sp_mapping_mode );
272                                         gtk_combo_box_text_append_text( combo, globalMappingMode().mp_mapping_mode );
273
274                                         combo.show();
275                     table2.attach(combo, {1, 2, 3, 4}, {GTK_EXPAND | GTK_FILL, 0});
276
277                                         dialog.gamemode_combo = combo;
278                                 }
279                         }
280                 }
281         }
282
283         // initialise the fs_game selection from the project settings into the dialog
284         const char* dir = gamename_get();
285         gamecombo_t gamecombo = gamecombo_for_dir( dir );
286
287         gtk_combo_box_set_active( dialog.game_combo.game_select, gamecombo.game );
288         dialog.game_combo.fsgame_entry.text( gamecombo.fs_game );
289         gtk_widget_set_sensitive( dialog.game_combo.fsgame_entry , gamecombo.sensitive );
290
291         if ( globalMappingMode().do_mapping_mode ) {
292                 const char *gamemode = gamemode_get();
293                 if ( string_empty( gamemode ) || string_equal( gamemode, "sp" ) ) {
294                         gtk_combo_box_set_active( dialog.gamemode_combo, 0 );
295                 }
296                 else
297                 {
298                         gtk_combo_box_set_active( dialog.gamemode_combo, 1 );
299                 }
300         }
301
302         return window;
303 }
304
305 void ProjectSettingsDialog_ok( ProjectSettingsDialog& dialog ){
306         const char* dir = gtk_entry_get_text( dialog.game_combo.fsgame_entry );
307
308         const char* new_gamename = path_equal( dir, globalGameComboConfiguration().basegame_dir )
309                                                            ? ""
310                                                            : dir;
311
312         if ( !path_equal( new_gamename, gamename_get() ) ) {
313                 ScopeDisableScreenUpdates disableScreenUpdates( "Processing...", "Changing Game Name" );
314
315                 EnginePath_Unrealise();
316
317                 gamename_set( new_gamename );
318
319                 EnginePath_Realise();
320         }
321
322         if ( globalMappingMode().do_mapping_mode ) {
323                 // read from gamemode_combo
324                 int active = gtk_combo_box_get_active( dialog.gamemode_combo );
325                 if ( active == -1 || active == 0 ) {
326                         gamemode_set( "sp" );
327                 }
328                 else
329                 {
330                         gamemode_set( "mp" );
331                 }
332         }
333 }
334
335 void DoProjectSettings(){
336         if ( ConfirmModified( "Edit Project Settings" ) ) {
337                 ModalDialog modal;
338                 ProjectSettingsDialog dialog;
339
340                 ui::Window window = ProjectSettingsDialog_construct( dialog, modal );
341
342                 if ( modal_dialog_show( window, modal ) == eIDOK ) {
343                         ProjectSettingsDialog_ok( dialog );
344                 }
345
346                 window.destroy();
347         }
348 }
349
350 // =============================================================================
351 // Arbitrary Sides dialog
352
353 void DoSides( int type, int axis ){
354         ModalDialog dialog;
355
356         auto window = MainFrame_getWindow().create_dialog_window("Arbitrary sides", G_CALLBACK(dialog_delete_callback ), &dialog );
357
358         auto accel = ui::AccelGroup(ui::New);
359         window.add_accel_group( accel );
360
361         auto sides_entry = ui::Entry(ui::New);
362         {
363                 auto hbox = create_dialog_hbox( 4, 4 );
364                 window.add(hbox);
365                 {
366                         auto label = ui::Label( "Sides:" );
367                         label.show();
368                         hbox.pack_start( label, FALSE, FALSE, 0 );
369                 }
370                 {
371                         auto entry = sides_entry;
372                         entry.show();
373                         hbox.pack_start( entry, FALSE, FALSE, 0 );
374                         gtk_widget_grab_focus( entry  );
375                 }
376                 {
377             auto vbox = create_dialog_vbox( 4 );
378                         hbox.pack_start( vbox, TRUE, TRUE, 0 );
379                         {
380                                 auto button = create_dialog_button( "OK", G_CALLBACK( dialog_button_ok ), &dialog );
381                                 vbox.pack_start( button, FALSE, FALSE, 0 );
382                                 widget_make_default( button );
383                                 gtk_widget_add_accelerator( button , "clicked", accel, GDK_KEY_Return, (GdkModifierType)0, (GtkAccelFlags)0 );
384                         }
385                         {
386                 auto button = create_dialog_button( "Cancel", G_CALLBACK( dialog_button_cancel ), &dialog );
387                                 vbox.pack_start( button, FALSE, FALSE, 0 );
388                                 gtk_widget_add_accelerator( button , "clicked", accel, GDK_KEY_Escape, (GdkModifierType)0, (GtkAccelFlags)0 );
389                         }
390                 }
391         }
392
393         if ( modal_dialog_show( window, dialog ) == eIDOK ) {
394                 const char *str = gtk_entry_get_text( sides_entry );
395
396                 Scene_BrushConstructPrefab( GlobalSceneGraph(), (EBrushPrefab)type, atoi( str ), TextureBrowser_GetSelectedShader( GlobalTextureBrowser() ) );
397         }
398
399         window.destroy();
400 }
401
402 // =============================================================================
403 // About dialog (no program is complete without one)
404
405 void about_button_changelog( ui::Widget widget, gpointer data ){
406         StringOutputStream log( 256 );
407         log << "https://gitlab.com/xonotic/netradiant/commits/master";
408         OpenURL( log.c_str() );
409 }
410
411 void about_button_credits( ui::Widget widget, gpointer data ){
412         StringOutputStream cred( 256 );
413         cred << "https://gitlab.com/xonotic/netradiant/graphs/master";
414         OpenURL( cred.c_str() );
415 }
416
417 void about_button_issues( ui::Widget widget, gpointer data ){
418         StringOutputStream cred( 256 );
419         cred << "https://gitlab.com/xonotic/netradiant/issues";
420         OpenURL( cred.c_str() );
421 }
422
423 void DoAbout(){
424         ModalDialog dialog;
425         ModalDialogButton ok_button( dialog, eIDOK );
426
427         auto window = MainFrame_getWindow().create_modal_dialog_window("About " RADIANT_NAME, dialog );
428
429         {
430                 auto vbox = create_dialog_vbox( 4, 4 );
431                 window.add(vbox);
432
433                 {
434             auto hbox = create_dialog_hbox( 4 );
435                         vbox.pack_start( hbox, FALSE, TRUE, 0 );
436
437                         {
438                 auto vbox2 = create_dialog_vbox( 4 );
439                                 hbox.pack_start( vbox2, TRUE, FALSE, 0 );
440                                 {
441                                         auto frame = create_dialog_frame( 0, ui::Shadow::IN );
442                                         vbox2.pack_start( frame, FALSE, FALSE, 0 );
443                                         {
444                                                 auto image = new_local_image( "logo.png" );
445                                                 image.show();
446                                                 frame.add(image);
447                                         }
448                                 }
449                         }
450
451                         {
452                                 char const *label_text = RADIANT_NAME " " RADIANT_VERSION_STRING "\n"
453                                                                                 __DATE__ "\n\n"
454                                                                                 RADIANT_ABOUTMSG "\n\n"
455                                                                                 "This program is free software\n"
456                                                                                 "licensed under the GNU GPL.\n\n"
457                                                                                 RADIANT_NAME " is unsupported, however\n"
458                                                                                 "you may report your problems at\n"
459                                                                                 "https://gitlab.com/xonotic/netradiant/issues";
460
461                                 auto label = ui::Label( label_text );
462
463                                 label.show();
464                                 hbox.pack_start( label, FALSE, FALSE, 0 );
465                                 gtk_misc_set_alignment( GTK_MISC( label ), 1, 0.5 );
466                                 gtk_label_set_justify( label, GTK_JUSTIFY_LEFT );
467                         }
468
469                         {
470                 auto vbox2 = create_dialog_vbox( 4 );
471                                 hbox.pack_start( vbox2, FALSE, TRUE, 0 );
472                                 {
473                     auto button = create_modal_dialog_button( "OK", ok_button );
474                                         vbox2.pack_start( button, FALSE, FALSE, 0 );
475                                 }
476                                 {
477                     auto button = create_dialog_button( "Credits", G_CALLBACK( about_button_credits ), 0 );
478                                         vbox2.pack_start( button, FALSE, FALSE, 0 );
479                                 }
480                                 {
481                     auto button = create_dialog_button( "Changes", G_CALLBACK( about_button_changelog ), 0 );
482                                         vbox2.pack_start( button, FALSE, FALSE, 0 );
483                                 }
484                                 {
485                     auto button = create_dialog_button( "Issues", G_CALLBACK( about_button_issues ), 0 );
486                                         vbox2.pack_start( button, FALSE, FALSE, 0 );
487                                 }
488                         }
489                 }
490                 {
491                         auto frame = create_dialog_frame( "OpenGL Properties" );
492                         vbox.pack_start( frame, FALSE, FALSE, 0 );
493                         {
494                                 auto table = create_dialog_table( 3, 2, 4, 4, 4 );
495                                 frame.add(table);
496                                 {
497                                         auto label = ui::Label( "Vendor:" );
498                                         label.show();
499                     table.attach(label, {0, 1, 0, 1}, {GTK_FILL, 0});
500                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
501                                 }
502                                 {
503                                         auto label = ui::Label( "Version:" );
504                                         label.show();
505                     table.attach(label, {0, 1, 1, 2}, {GTK_FILL, 0});
506                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
507                                 }
508                                 {
509                                         auto label = ui::Label( "Renderer:" );
510                                         label.show();
511                     table.attach(label, {0, 1, 2, 3}, {GTK_FILL, 0});
512                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
513                                 }
514                                 {
515                                         auto label = ui::Label( reinterpret_cast<const char*>( glGetString( GL_VENDOR ) ) );
516                                         label.show();
517                     table.attach(label, {1, 2, 0, 1}, {GTK_FILL, 0});
518                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
519                                 }
520                                 {
521                                         auto label = ui::Label( reinterpret_cast<const char*>( glGetString( GL_VERSION ) ) );
522                                         label.show();
523                     table.attach(label, {1, 2, 1, 2}, {GTK_FILL, 0});
524                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
525                                 }
526                                 {
527                                         auto label = ui::Label( reinterpret_cast<const char*>( glGetString( GL_RENDERER ) ) );
528                                         label.show();
529                     table.attach(label, {1, 2, 2, 3}, {GTK_FILL, 0});
530                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
531                                 }
532                         }
533                         {
534                                 auto frame = create_dialog_frame( "OpenGL Extensions" );
535                                 vbox.pack_start( frame, TRUE, TRUE, 0 );
536                                 {
537                                         auto sc_extensions = create_scrolled_window( ui::Policy::AUTOMATIC, ui::Policy::ALWAYS, 4 );
538                                         frame.add(sc_extensions);
539                                         {
540                                                 auto text_extensions = ui::TextView(ui::New);
541                                                 gtk_text_view_set_editable( text_extensions, FALSE );
542                                                 sc_extensions.add(text_extensions);
543                                                 text_extensions.text(reinterpret_cast<const char *>(glGetString(GL_EXTENSIONS)));
544                                                 gtk_text_view_set_wrap_mode( text_extensions, GTK_WRAP_WORD );
545                                                 text_extensions.show();
546                                         }
547                                 }
548                         }
549                 }
550         }
551
552         modal_dialog_show( window, dialog );
553
554         window.destroy();
555 }
556
557 // =============================================================================
558 // TextureLayout dialog
559
560 // Last used texture scale values
561 static float last_used_texture_layout_scale_x = 4.0;
562 static float last_used_texture_layout_scale_y = 4.0;
563
564 EMessageBoxReturn DoTextureLayout( float *fx, float *fy ){
565         ModalDialog dialog;
566         ModalDialogButton ok_button( dialog, eIDOK );
567         ModalDialogButton cancel_button( dialog, eIDCANCEL );
568         ui::Entry x{ui::null};
569         ui::Entry y{ui::null};
570
571         auto window = MainFrame_getWindow().create_modal_dialog_window("Patch texture layout", dialog );
572
573         auto accel = ui::AccelGroup(ui::New);
574         window.add_accel_group( accel );
575
576         {
577                 auto hbox = create_dialog_hbox( 4, 4 );
578                 window.add(hbox);
579                 {
580             auto vbox = create_dialog_vbox( 4 );
581                         hbox.pack_start( vbox, TRUE, TRUE, 0 );
582                         {
583                                 auto label = ui::Label( "Texture will be fit across the patch based\n"
584                                                                                                                         "on the x and y values given. Values of 1x1\n"
585                                                                                                                         "will \"fit\" the texture. 2x2 will repeat\n"
586                                                                                                                         "it twice, etc." );
587                                 label.show();
588                                 vbox.pack_start( label, TRUE, TRUE, 0 );
589                                 gtk_label_set_justify( label, GTK_JUSTIFY_LEFT );
590                         }
591                         {
592                                 auto table = create_dialog_table( 2, 2, 4, 4 );
593                                 table.show();
594                                 vbox.pack_start( table, TRUE, TRUE, 0 );
595                                 {
596                                         auto label = ui::Label( "Texture x:" );
597                                         label.show();
598                     table.attach(label, {0, 1, 0, 1}, {GTK_FILL, 0});
599                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
600                                 }
601                                 {
602                                         auto label = ui::Label( "Texture y:" );
603                                         label.show();
604                     table.attach(label, {0, 1, 1, 2}, {GTK_FILL, 0});
605                                         gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );
606                                 }
607                                 {
608                                         auto entry = ui::Entry(ui::New);
609                                         entry.show();
610                     table.attach(entry, {1, 2, 0, 1}, {GTK_EXPAND | GTK_FILL, 0});
611
612                                         x = entry;
613                                 }
614                                 {
615                                         auto entry = ui::Entry(ui::New);
616                                         entry.show();
617                     table.attach(entry, {1, 2, 1, 2}, {GTK_EXPAND | GTK_FILL, 0});
618
619                                         y = entry;
620                                 }
621                         }
622                 }
623                 {
624             auto vbox = create_dialog_vbox( 4 );
625                         hbox.pack_start( vbox, FALSE, FALSE, 0 );
626                         {
627                                 auto button = create_modal_dialog_button( "OK", ok_button );
628                                 vbox.pack_start( button, FALSE, FALSE, 0 );
629                                 widget_make_default( button );
630                                 gtk_widget_add_accelerator( button , "clicked", accel, GDK_KEY_Return, (GdkModifierType)0, (GtkAccelFlags)0 );
631                         }
632                         {
633                 auto button = create_modal_dialog_button( "Cancel", cancel_button );
634                                 vbox.pack_start( button, FALSE, FALSE, 0 );
635                                 gtk_widget_add_accelerator( button , "clicked", accel, GDK_KEY_Escape, (GdkModifierType)0, (GtkAccelFlags)0 );
636                         }
637                 }
638         }
639         
640         // Initialize with last used values
641         char buf[16];
642         
643         sprintf( buf, "%f", last_used_texture_layout_scale_x );
644         x.text( buf );
645         
646         sprintf( buf, "%f", last_used_texture_layout_scale_y );
647         y.text( buf );
648
649         // Set focus after intializing the values
650         gtk_widget_grab_focus( x  );
651
652         EMessageBoxReturn ret = modal_dialog_show( window, dialog );
653         if ( ret == eIDOK ) {
654                 *fx = static_cast<float>( atof( gtk_entry_get_text( x ) ) );
655                 *fy = static_cast<float>( atof( gtk_entry_get_text( y ) ) );
656         
657                 // Remember last used values
658                 last_used_texture_layout_scale_x = *fx;
659                 last_used_texture_layout_scale_y = *fy;
660         }
661
662         window.destroy();
663
664         return ret;
665 }
666
667 // =============================================================================
668 // Text Editor dialog
669
670 // master window widget
671 static ui::Window text_editor{ui::null};
672 static ui::Widget text_widget{ui::null}; // slave, text widget from the gtk editor
673
674 static gint editor_delete( ui::Widget widget, gpointer data ){
675         if ( ui::alert( widget.window(), "Close the shader editor ?", "Radiant", ui::alert_type::YESNO, ui::alert_icon::Question ) == ui::alert_response::NO ) {
676                 return TRUE;
677         }
678
679         text_editor.hide();
680
681         return TRUE;
682 }
683
684 static void editor_save( ui::Widget widget, gpointer data ){
685         FILE *f = fopen( (char*)g_object_get_data( G_OBJECT( data ), "filename" ), "w" );
686         gpointer text = g_object_get_data( G_OBJECT( data ), "text" );
687
688         if ( f == 0 ) {
689                 ui::alert( ui::Widget::from(data).window(), "Error saving file !" );
690                 return;
691         }
692
693         char *str = gtk_editable_get_chars( GTK_EDITABLE( text ), 0, -1 );
694         fwrite( str, 1, strlen( str ), f );
695         fclose( f );
696 }
697
698 static void editor_close( ui::Widget widget, gpointer data ){
699         if ( ui::alert( text_editor.window(), "Close the shader editor ?", "Radiant", ui::alert_type::YESNO, ui::alert_icon::Question ) == ui::alert_response::NO ) {
700                 return;
701         }
702
703         text_editor.hide();
704 }
705
706 static void CreateGtkTextEditor(){
707         auto dlg = ui::Window( ui::window_type::TOP );
708
709         dlg.connect( "delete_event",
710                                           G_CALLBACK( editor_delete ), 0 );
711         gtk_window_set_default_size( dlg, 600, 300 );
712
713         auto vbox = ui::VBox( FALSE, 5 );
714         vbox.show();
715         dlg.add(vbox);
716         gtk_container_set_border_width( GTK_CONTAINER( vbox ), 5 );
717
718         auto scr = ui::ScrolledWindow(ui::New);
719         scr.show();
720         vbox.pack_start( scr, TRUE, TRUE, 0 );
721         gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scr ), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC );
722         gtk_scrolled_window_set_shadow_type( GTK_SCROLLED_WINDOW( scr ), GTK_SHADOW_IN );
723
724         auto text = ui::TextView(ui::New);
725         scr.add(text);
726         text.show();
727         g_object_set_data( G_OBJECT( dlg ), "text", (gpointer) text );
728         gtk_text_view_set_editable( text, TRUE );
729
730         auto hbox = ui::HBox( FALSE, 5 );
731         hbox.show();
732         vbox.pack_start( hbox, FALSE, TRUE, 0 );
733
734         auto button = ui::Button( "Close" );
735         button.show();
736         hbox.pack_end(button, FALSE, FALSE, 0);
737         button.connect( "clicked",
738                                           G_CALLBACK( editor_close ), dlg );
739         button.dimensions(60, -1);
740
741         button = ui::Button( "Save" );
742         button.show();
743         hbox.pack_end(button, FALSE, FALSE, 0);
744         button.connect( "clicked",
745                                           G_CALLBACK( editor_save ), dlg );
746         button.dimensions(60, -1);
747
748         text_editor = dlg;
749         text_widget = text;
750 }
751
752 static void DoGtkTextEditor( const char* filename, guint cursorpos ){
753         if ( !text_editor ) {
754                 CreateGtkTextEditor(); // build it the first time we need it
755
756         }
757         // Load file
758         FILE *f = fopen( filename, "r" );
759
760         if ( f == 0 ) {
761                 globalOutputStream() << "Unable to load file " << filename << " in shader editor.\n";
762                 text_editor.hide();
763         }
764         else
765         {
766                 fseek( f, 0, SEEK_END );
767                 int len = ftell( f );
768                 void *buf = malloc( len );
769                 void *old_filename;
770
771                 rewind( f );
772                 fread( buf, 1, len, f );
773
774                 gtk_window_set_title( text_editor, filename );
775
776                 auto text_buffer = gtk_text_view_get_buffer(ui::TextView::from(text_widget));
777                 gtk_text_buffer_set_text( text_buffer, (char*)buf, len );
778
779                 old_filename = g_object_get_data( G_OBJECT( text_editor ), "filename" );
780                 if ( old_filename ) {
781                         free( old_filename );
782                 }
783                 g_object_set_data( G_OBJECT( text_editor ), "filename", strdup( filename ) );
784
785                 // trying to show later
786                 text_editor.show();
787
788 #if GDEF_OS_WINDOWS
789                 ui::process();
790 #endif
791
792                 // only move the cursor if it's not exceeding the size..
793                 // NOTE: this is erroneous, cursorpos is the offset in bytes, not in characters
794                 // len is the max size in bytes, not in characters either, but the character count is below that limit..
795                 // thinking .. the difference between character count and byte count would be only because of CR/LF?
796                 {
797                         GtkTextIter text_iter;
798                         // character offset, not byte offset
799                         gtk_text_buffer_get_iter_at_offset( text_buffer, &text_iter, cursorpos );
800                         gtk_text_buffer_place_cursor( text_buffer, &text_iter );
801                 }
802
803 #if GDEF_OS_WINDOWS
804                 gtk_widget_queue_draw( text_widget );
805 #endif
806
807                 free( buf );
808                 fclose( f );
809         }
810 }
811
812 // =============================================================================
813 // Light Intensity dialog
814
815 EMessageBoxReturn DoLightIntensityDlg( int *intensity ){
816         ModalDialog dialog;
817         ui::Entry intensity_entry{ui::null};
818         ModalDialogButton ok_button( dialog, eIDOK );
819         ModalDialogButton cancel_button( dialog, eIDCANCEL );
820
821         ui::Window window = MainFrame_getWindow().create_modal_dialog_window("Light intensity", dialog, -1, -1 );
822
823         auto accel_group = ui::AccelGroup(ui::New);
824         window.add_accel_group( accel_group );
825
826         {
827                 auto hbox = create_dialog_hbox( 4, 4 );
828                 window.add(hbox);
829                 {
830             auto vbox = create_dialog_vbox( 4 );
831                         hbox.pack_start( vbox, TRUE, TRUE, 0 );
832                         {
833                                 auto label = ui::Label( "ESC for default, ENTER to validate" );
834                                 label.show();
835                                 vbox.pack_start( label, FALSE, FALSE, 0 );
836                         }
837                         {
838                                 auto entry = ui::Entry(ui::New);
839                                 entry.show();
840                                 vbox.pack_start( entry, TRUE, TRUE, 0 );
841
842                                 gtk_widget_grab_focus( entry  );
843
844                                 intensity_entry = entry;
845                         }
846                 }
847                 {
848             auto vbox = create_dialog_vbox( 4 );
849                         hbox.pack_start( vbox, FALSE, FALSE, 0 );
850
851                         {
852                                 auto button = create_modal_dialog_button( "OK", ok_button );
853                                 vbox.pack_start( button, FALSE, FALSE, 0 );
854                                 widget_make_default( button );
855                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Return, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
856                         }
857                         {
858                 auto button = create_modal_dialog_button( "Cancel", cancel_button );
859                                 vbox.pack_start( button, FALSE, FALSE, 0 );
860                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Escape, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
861                         }
862                 }
863         }
864
865         char buf[16];
866         sprintf( buf, "%d", *intensity );
867         intensity_entry.text(buf);
868
869         EMessageBoxReturn ret = modal_dialog_show( window, dialog );
870         if ( ret == eIDOK ) {
871                 *intensity = atoi( gtk_entry_get_text( intensity_entry ) );
872         }
873
874         window.destroy();
875
876         return ret;
877 }
878
879 // =============================================================================
880 // Add new shader tag dialog
881
882 EMessageBoxReturn DoShaderTagDlg( CopiedString* tag, const char* title ){
883         ModalDialog dialog;
884         ModalDialogButton ok_button( dialog, eIDOK );
885         ModalDialogButton cancel_button( dialog, eIDCANCEL );
886
887         auto window = MainFrame_getWindow().create_modal_dialog_window(title, dialog, -1, -1 );
888
889         auto accel_group = ui::AccelGroup(ui::New);
890         window.add_accel_group( accel_group );
891
892         auto textentry = ui::Entry(ui::New);
893         {
894                 auto hbox = create_dialog_hbox( 4, 4 );
895                 window.add(hbox);
896                 {
897             auto vbox = create_dialog_vbox( 4 );
898                         hbox.pack_start( vbox, TRUE, TRUE, 0 );
899                         {
900                                 //GtkLabel* label = GTK_LABEL(gtk_label_new("Enter one ore more tags separated by spaces"));
901                                 auto label = ui::Label( "ESC to cancel, ENTER to validate" );
902                                 label.show();
903                                 vbox.pack_start( label, FALSE, FALSE, 0 );
904                         }
905                         {
906                                 auto entry = textentry;
907                                 entry.show();
908                                 vbox.pack_start( entry, TRUE, TRUE, 0 );
909
910                                 gtk_widget_grab_focus( entry  );
911                         }
912                 }
913                 {
914             auto vbox = create_dialog_vbox( 4 );
915                         hbox.pack_start( vbox, FALSE, FALSE, 0 );
916
917                         {
918                                 auto button = create_modal_dialog_button( "OK", ok_button );
919                                 vbox.pack_start( button, FALSE, FALSE, 0 );
920                                 widget_make_default( button );
921                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Return, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
922                         }
923                         {
924                 auto button = create_modal_dialog_button( "Cancel", cancel_button );
925                                 vbox.pack_start( button, FALSE, FALSE, 0 );
926                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Escape, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
927                         }
928                 }
929         }
930
931         EMessageBoxReturn ret = modal_dialog_show( window, dialog );
932         if ( ret == eIDOK ) {
933                 *tag = gtk_entry_get_text( textentry );
934         }
935
936         window.destroy();
937
938         return ret;
939 }
940
941 EMessageBoxReturn DoShaderInfoDlg( const char* name, const char* filename, const char* title ){
942         ModalDialog dialog;
943         ModalDialogButton ok_button( dialog, eIDOK );
944
945         auto window = MainFrame_getWindow().create_modal_dialog_window(title, dialog, -1, -1 );
946
947         auto accel_group = ui::AccelGroup(ui::New);
948         window.add_accel_group( accel_group );
949
950         {
951                 auto hbox = create_dialog_hbox( 4, 4 );
952                 window.add(hbox);
953                 {
954             auto vbox = create_dialog_vbox( 4 );
955                         hbox.pack_start( vbox, FALSE, FALSE, 0 );
956                         {
957                                 auto label = ui::Label( "The selected shader" );
958                                 label.show();
959                                 vbox.pack_start( label, FALSE, FALSE, 0 );
960                         }
961                         {
962                                 auto label = ui::Label( name );
963                                 label.show();
964                                 vbox.pack_start( label, FALSE, FALSE, 0 );
965                         }
966                         {
967                                 auto label = ui::Label( "is located in file" );
968                                 label.show();
969                                 vbox.pack_start( label, FALSE, FALSE, 0 );
970                         }
971                         {
972                                 auto label = ui::Label( filename );
973                                 label.show();
974                                 vbox.pack_start( label, FALSE, FALSE, 0 );
975                         }
976                         {
977                                 auto button = create_modal_dialog_button( "OK", ok_button );
978                                 vbox.pack_start( button, FALSE, FALSE, 0 );
979                                 widget_make_default( button );
980                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Return, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
981                         }
982                 }
983         }
984
985         EMessageBoxReturn ret = modal_dialog_show( window, dialog );
986
987         window.destroy();
988
989         return ret;
990 }
991
992
993
994 #if GDEF_OS_WINDOWS
995 #include <gdk/gdkwin32.h>
996 #endif
997
998 #if GDEF_OS_WINDOWS
999 // use the file associations to open files instead of builtin Gtk editor
1000 bool g_TextEditor_useWin32Editor = true;
1001 #else
1002 // custom shader editor
1003 bool g_TextEditor_useCustomEditor = false;
1004 CopiedString g_TextEditor_editorCommand( "" );
1005 #endif
1006
1007 void DoTextEditor( const char* filename, int cursorpos ){
1008 #if GDEF_OS_WINDOWS
1009         if ( g_TextEditor_useWin32Editor ) {
1010                 globalOutputStream() << "opening file '" << filename << "' (line " << cursorpos << " info ignored)\n";
1011                 ShellExecute( (HWND)GDK_WINDOW_HWND( gtk_widget_get_window( MainFrame_getWindow() ) ), "open", filename, 0, 0, SW_SHOW );
1012                 return;
1013         }
1014 #else
1015         // check if a custom editor is set
1016         if ( g_TextEditor_useCustomEditor && !g_TextEditor_editorCommand.empty() ) {
1017                 StringOutputStream strEditCommand( 256 );
1018                 strEditCommand << g_TextEditor_editorCommand.c_str() << " \"" << filename << "\"";
1019
1020                 globalOutputStream() << "Launching: " << strEditCommand.c_str() << "\n";
1021                 // note: linux does not return false if the command failed so it will assume success
1022                 if ( Q_Exec( 0, const_cast<char*>( strEditCommand.c_str() ), 0, true, false ) == false ) {
1023                         globalOutputStream() << "Failed to execute " << strEditCommand.c_str() << ", using default\n";
1024                 }
1025                 else
1026                 {
1027                         // the command (appeared) to run successfully, no need to do anything more
1028                         return;
1029                 }
1030         }
1031 #endif
1032
1033         DoGtkTextEditor( filename, cursorpos );
1034 }