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