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