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