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