]> de.git.xonotic.org Git - xonotic/netradiant.git/blob - radiant/gtkdlgs.cpp
Merge commit '9d6e27e4e37e7406e8691594fb0b74aab89e954b' 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                 gtk_window_present( GTK_WINDOW( text_editor ) );
803
804 #if GDEF_OS_WINDOWS
805                 ui::process();
806 #endif
807
808                 // only move the cursor if it's not exceeding the size..
809                 // NOTE: this is erroneous, cursorpos is the offset in bytes, not in characters
810                 // len is the max size in bytes, not in characters either, but the character count is below that limit..
811                 // thinking .. the difference between character count and byte count would be only because of CR/LF?
812                 {
813                         GtkTextIter text_iter;
814                         // character offset, not byte offset
815                         gtk_text_buffer_get_iter_at_offset( text_buffer, &text_iter, cursorpos );
816                         gtk_text_buffer_place_cursor( text_buffer, &text_iter );
817                         gtk_text_view_scroll_to_iter( GTK_TEXT_VIEW( text_widget ), &text_iter, 0, TRUE, 0, 0);
818                 }
819
820 #if GDEF_OS_WINDOWS
821                 gtk_widget_queue_draw( text_widget );
822 #endif
823
824                 text_buffer_ = text_buffer;
825                 free( buf );
826                 fclose( f );
827         }
828 }
829
830 // =============================================================================
831 // Light Intensity dialog
832
833 EMessageBoxReturn DoLightIntensityDlg( int *intensity ){
834         ModalDialog dialog;
835         ui::Entry intensity_entry{ui::null};
836         ModalDialogButton ok_button( dialog, eIDOK );
837         ModalDialogButton cancel_button( dialog, eIDCANCEL );
838
839         ui::Window window = MainFrame_getWindow().create_modal_dialog_window("Light intensity", dialog, -1, -1 );
840
841         auto accel_group = ui::AccelGroup(ui::New);
842         window.add_accel_group( accel_group );
843
844         {
845                 auto hbox = create_dialog_hbox( 4, 4 );
846                 window.add(hbox);
847                 {
848             auto vbox = create_dialog_vbox( 4 );
849                         hbox.pack_start( vbox, TRUE, TRUE, 0 );
850                         {
851                                 auto label = ui::Label( "ESC for default, ENTER to validate" );
852                                 label.show();
853                                 vbox.pack_start( label, FALSE, FALSE, 0 );
854                         }
855                         {
856                                 auto entry = ui::Entry(ui::New);
857                                 entry.show();
858                                 vbox.pack_start( entry, TRUE, TRUE, 0 );
859
860                                 gtk_widget_grab_focus( entry  );
861
862                                 intensity_entry = entry;
863                         }
864                 }
865                 {
866             auto vbox = create_dialog_vbox( 4 );
867                         hbox.pack_start( vbox, FALSE, FALSE, 0 );
868
869                         {
870                                 auto button = create_modal_dialog_button( "OK", ok_button );
871                                 vbox.pack_start( button, FALSE, FALSE, 0 );
872                                 widget_make_default( button );
873                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Return, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
874                         }
875                         {
876                 auto button = create_modal_dialog_button( "Cancel", cancel_button );
877                                 vbox.pack_start( button, FALSE, FALSE, 0 );
878                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Escape, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
879                         }
880                 }
881         }
882
883         char buf[16];
884         sprintf( buf, "%d", *intensity );
885         intensity_entry.text(buf);
886
887         EMessageBoxReturn ret = modal_dialog_show( window, dialog );
888         if ( ret == eIDOK ) {
889                 *intensity = atoi( gtk_entry_get_text( intensity_entry ) );
890         }
891
892         window.destroy();
893
894         return ret;
895 }
896
897 // =============================================================================
898 // Add new shader tag dialog
899
900 EMessageBoxReturn DoShaderTagDlg( CopiedString* tag, const char* title ){
901         ModalDialog dialog;
902         ModalDialogButton ok_button( dialog, eIDOK );
903         ModalDialogButton cancel_button( dialog, eIDCANCEL );
904
905         auto window = MainFrame_getWindow().create_modal_dialog_window(title, dialog, -1, -1 );
906
907         auto accel_group = ui::AccelGroup(ui::New);
908         window.add_accel_group( accel_group );
909
910         auto textentry = ui::Entry(ui::New);
911         {
912                 auto hbox = create_dialog_hbox( 4, 4 );
913                 window.add(hbox);
914                 {
915             auto vbox = create_dialog_vbox( 4 );
916                         hbox.pack_start( vbox, TRUE, TRUE, 0 );
917                         {
918                                 //GtkLabel* label = GTK_LABEL(gtk_label_new("Enter one ore more tags separated by spaces"));
919                                 auto label = ui::Label( "ESC to cancel, ENTER to validate" );
920                                 label.show();
921                                 vbox.pack_start( label, FALSE, FALSE, 0 );
922                         }
923                         {
924                                 auto entry = textentry;
925                                 entry.show();
926                                 vbox.pack_start( entry, TRUE, TRUE, 0 );
927
928                                 gtk_widget_grab_focus( entry  );
929                         }
930                 }
931                 {
932             auto vbox = create_dialog_vbox( 4 );
933                         hbox.pack_start( vbox, FALSE, FALSE, 0 );
934
935                         {
936                                 auto button = create_modal_dialog_button( "OK", ok_button );
937                                 vbox.pack_start( button, FALSE, FALSE, 0 );
938                                 widget_make_default( button );
939                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Return, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
940                         }
941                         {
942                 auto button = create_modal_dialog_button( "Cancel", cancel_button );
943                                 vbox.pack_start( button, FALSE, FALSE, 0 );
944                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Escape, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
945                         }
946                 }
947         }
948
949         EMessageBoxReturn ret = modal_dialog_show( window, dialog );
950         if ( ret == eIDOK ) {
951                 *tag = gtk_entry_get_text( textentry );
952         }
953
954         window.destroy();
955
956         return ret;
957 }
958
959 EMessageBoxReturn DoShaderInfoDlg( const char* name, const char* filename, const char* title ){
960         ModalDialog dialog;
961         ModalDialogButton ok_button( dialog, eIDOK );
962
963         auto window = MainFrame_getWindow().create_modal_dialog_window(title, dialog, -1, -1 );
964
965         auto accel_group = ui::AccelGroup(ui::New);
966         window.add_accel_group( accel_group );
967
968         {
969                 auto hbox = create_dialog_hbox( 4, 4 );
970                 window.add(hbox);
971                 {
972             auto vbox = create_dialog_vbox( 4 );
973                         hbox.pack_start( vbox, FALSE, FALSE, 0 );
974                         {
975                                 auto label = ui::Label( "The selected shader" );
976                                 label.show();
977                                 vbox.pack_start( label, FALSE, FALSE, 0 );
978                         }
979                         {
980                                 auto label = ui::Label( name );
981                                 label.show();
982                                 vbox.pack_start( label, FALSE, FALSE, 0 );
983                         }
984                         {
985                                 auto label = ui::Label( "is located in file" );
986                                 label.show();
987                                 vbox.pack_start( label, FALSE, FALSE, 0 );
988                         }
989                         {
990                                 auto label = ui::Label( filename );
991                                 label.show();
992                                 vbox.pack_start( label, FALSE, FALSE, 0 );
993                         }
994                         {
995                                 auto button = create_modal_dialog_button( "OK", ok_button );
996                                 vbox.pack_start( button, FALSE, FALSE, 0 );
997                                 widget_make_default( button );
998                                 gtk_widget_add_accelerator( button , "clicked", accel_group, GDK_KEY_Return, (GdkModifierType)0, GTK_ACCEL_VISIBLE );
999                         }
1000                 }
1001         }
1002
1003         EMessageBoxReturn ret = modal_dialog_show( window, dialog );
1004
1005         window.destroy();
1006
1007         return ret;
1008 }
1009
1010
1011
1012 #if GDEF_OS_WINDOWS
1013 #include <gdk/gdkwin32.h>
1014 #endif
1015
1016 #if GDEF_OS_WINDOWS
1017 // use the file associations to open files instead of builtin Gtk editor
1018 bool g_TextEditor_useWin32Editor = false;
1019 #else
1020 // custom shader editor
1021 bool g_TextEditor_useCustomEditor = false;
1022 CopiedString g_TextEditor_editorCommand( "" );
1023 #endif
1024
1025 void DoTextEditor( const char* filename, int cursorpos, int length ){
1026 #if GDEF_OS_WINDOWS
1027         if ( g_TextEditor_useWin32Editor ) {
1028                 StringOutputStream path( 256 );
1029                 StringOutputStream modpath( 256 );
1030                 const char* gamename = GlobalRadiant().getGameName();
1031                 const char* basegame = GlobalRadiant().getRequiredGameDescriptionKeyValue( "basegame" );
1032                 const char* enginePath = GlobalRadiant().getEnginePath();
1033                 path << enginePath << basegame << '/' << filename;
1034                 modpath << enginePath << gamename << '/' << filename;
1035                 if ( file_exists( modpath.c_str() ) ){
1036                         globalOutputStream() << "opening file '" << modpath.c_str() << "' (line " << cursorpos << " info ignored)\n";
1037                         ShellExecute( (HWND)GDK_WINDOW_HWND( gtk_widget_get_window( MainFrame_getWindow() ) ), "open", modpath.c_str(), 0, 0, SW_SHOW );
1038                 }
1039                 else if ( file_exists( path.c_str() ) ){
1040                         globalOutputStream() << "opening file '" << path.c_str() << "' (line " << cursorpos << " info ignored)\n";
1041                         ShellExecute( (HWND)GDK_WINDOW_HWND( gtk_widget_get_window( MainFrame_getWindow() ) ), "open", path.c_str(), 0, 0, SW_SHOW );
1042                 }
1043                 else{
1044                         globalOutputStream() << "Failed to open '" << filename << "'\nOne sits in .pk3 most likely!\n";
1045                 }
1046                 return;
1047         }
1048         else{
1049                 StringOutputStream path( 256 );
1050                 StringOutputStream modpath( 256 );
1051                 const char* gamename = GlobalRadiant().getGameName();
1052                 const char* basegame = GlobalRadiant().getRequiredGameDescriptionKeyValue( "basegame" );
1053                 const char* enginePath = GlobalRadiant().getEnginePath();
1054                 path << enginePath << basegame << '/' << filename;
1055                 modpath << enginePath << gamename << '/' << filename;
1056                 if ( file_exists( modpath.c_str() ) ){
1057                         globalOutputStream() << "opening file '" << modpath.c_str() << "' (line " << cursorpos << " info ignored)\n";
1058                         DoGtkTextEditor( modpath.c_str(), cursorpos, length );
1059                 }
1060                 else if ( file_exists( path.c_str() ) ){
1061                         globalOutputStream() << "opening file '" << path.c_str() << "' (line " << cursorpos << " info ignored)\n";
1062                         DoGtkTextEditor( path.c_str(), cursorpos, length );
1063                 }
1064                 else{
1065                         globalOutputStream() << "Failed to open '" << filename << "'\nOne sits in .pk3 most likely!\n";
1066                 }
1067                 return;
1068         }
1069 #else
1070         // check if a custom editor is set
1071         if ( g_TextEditor_useCustomEditor && !g_TextEditor_editorCommand.empty() ) {
1072                 StringOutputStream strEditCommand( 256 );
1073                 strEditCommand << g_TextEditor_editorCommand.c_str() << " \"" << filename << "\"";
1074
1075                 globalOutputStream() << "Launching: " << strEditCommand.c_str() << "\n";
1076                 // note: linux does not return false if the command failed so it will assume success
1077                 if ( Q_Exec( 0, const_cast<char*>( strEditCommand.c_str() ), 0, true, false ) == false ) {
1078                         globalOutputStream() << "Failed to execute " << strEditCommand.c_str() << ", using default\n";
1079                 }
1080                 else
1081                 {
1082                         // the command (appeared) to run successfully, no need to do anything more
1083                         return;
1084                 }
1085         }
1086
1087         DoGtkTextEditor( filename, cursorpos, length );
1088 #endif
1089 }