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