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