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