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