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