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