]> de.git.xonotic.org Git - xonotic/netradiant.git/blob - radiant/preferences.cpp
radiant: list games alphabetically
[xonotic/netradiant.git] / radiant / preferences.cpp
1 /*
2    Copyright (C) 1999-2006 Id Software, Inc. and contributors.
3    For a list of contributors, see the accompanying CONTRIBUTORS file.
4
5    This file is part of GtkRadiant.
6
7    GtkRadiant is free software; you can redistribute it and/or modify
8    it under the terms of the GNU General Public License as published by
9    the Free Software Foundation; either version 2 of the License, or
10    (at your option) any later version.
11
12    GtkRadiant is distributed in the hope that it will be useful,
13    but WITHOUT ANY WARRANTY; without even the implied warranty of
14    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15    GNU General Public License for more details.
16
17    You should have received a copy of the GNU General Public License
18    along with GtkRadiant; if not, write to the Free Software
19    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
20  */
21
22 //
23 // User preferences
24 //
25 // Leonardo Zide (leo@lokigames.com)
26 //
27
28 #include "preferences.h"
29 #include "globaldefs.h"
30
31 #include <gtk/gtk.h>
32 #include "environment.h"
33
34 #include "debugging/debugging.h"
35
36 #include "generic/callback.h"
37 #include "math/vector.h"
38 #include "string/string.h"
39 #include "stream/stringstream.h"
40 #include "os/file.h"
41 #include "os/path.h"
42 #include "os/dir.h"
43 #include "gtkutil/filechooser.h"
44 #include "gtkutil/messagebox.h"
45 #include "cmdlib.h"
46
47 #include "error.h"
48 #include "console.h"
49 #include "xywindow.h"
50 #include "mainframe.h"
51 #include "qe3.h"
52 #include "gtkdlgs.h"
53
54
55
56 void Global_constructPreferences( PreferencesPage& page ){
57         page.appendCheckBox( "Console", "Enable Logging", g_Console_enableLogging );
58 }
59
60 void Interface_constructPreferences( PreferencesPage& page ){
61 #if GDEF_OS_WINDOWS
62         page.appendCheckBox( "", "Default Text Editor", g_TextEditor_useWin32Editor );
63 #else
64         {
65                 ui::CheckButton use_custom = page.appendCheckBox( "Text Editor", "Custom", g_TextEditor_useCustomEditor );
66                 ui::Widget custom_editor = page.appendPathEntry( "Text Editor Command", g_TextEditor_editorCommand, true );
67                 Widget_connectToggleDependency( custom_editor, use_custom );
68         }
69 #endif
70 }
71
72 void Mouse_constructPreferences( PreferencesPage& page ){
73         {
74                 const char* buttons[] = { "2 button", "3 button", };
75                 page.appendRadio( "Mouse Type",  g_glwindow_globals.m_nMouseType, STRING_ARRAY_RANGE( buttons ) );
76         }
77         page.appendCheckBox( "Right Button", "Activates Context Menu", g_xywindow_globals.m_bRightClick );
78 }
79 void Mouse_constructPage( PreferenceGroup& group ){
80         PreferencesPage page( group.createPage( "Mouse", "Mouse Preferences" ) );
81         Mouse_constructPreferences( page );
82 }
83 void Mouse_registerPreferencesPage(){
84         PreferencesDialog_addInterfacePage( makeCallbackF(Mouse_constructPage) );
85 }
86
87
88 /*!
89    =========================================================
90    Games selection dialog
91    =========================================================
92  */
93
94 #include <map>
95 #include <uilib/uilib.h>
96
97 inline const char* xmlAttr_getName( xmlAttrPtr attr ){
98         return reinterpret_cast<const char*>( attr->name );
99 }
100
101 inline const char* xmlAttr_getValue( xmlAttrPtr attr ){
102         return reinterpret_cast<const char*>( attr->children->content );
103 }
104
105 CGameDescription::CGameDescription( xmlDocPtr pDoc, const CopiedString& gameFile ){
106         // read the user-friendly game name
107         xmlNodePtr pNode = pDoc->children;
108
109         while ( strcmp( (const char*)pNode->name, "game" ) && pNode != 0 )
110         {
111                 pNode = pNode->next;
112         }
113         if ( !pNode ) {
114                 Error( "Didn't find 'game' node in the game description file '%s'\n", pDoc->URL );
115         }
116
117         for ( xmlAttrPtr attr = pNode->properties; attr != 0; attr = attr->next )
118         {
119                 m_gameDescription.insert( GameDescription::value_type( xmlAttr_getName( attr ), xmlAttr_getValue( attr ) ) );
120         }
121
122         {
123                 StringOutputStream path( 256 );
124                 path << DataPath_get() << "gamepacks/" << gameFile.c_str() << "/";
125                 mGameToolsPath = path.c_str();
126         }
127
128         ASSERT_MESSAGE( file_exists( mGameToolsPath.c_str() ), "game directory not found: " << makeQuoted( mGameToolsPath.c_str() ) );
129
130         mGameFile = gameFile;
131
132         {
133                 GameDescription::iterator i = m_gameDescription.find( "type" );
134                 if ( i == m_gameDescription.end() ) {
135                         globalErrorStream() << "Warning, 'type' attribute not found in '" << reinterpret_cast<const char*>( pDoc->URL ) << "'\n";
136                         // default
137                         mGameType = "q3";
138                 }
139                 else
140                 {
141                         mGameType = ( *i ).second.c_str();
142                 }
143         }
144 }
145
146 void CGameDescription::Dump(){
147         globalOutputStream() << "game description file: " << makeQuoted( mGameFile.c_str() ) << "\n";
148         for ( GameDescription::iterator i = m_gameDescription.begin(); i != m_gameDescription.end(); ++i )
149         {
150                 globalOutputStream() << ( *i ).first.c_str() << " = " << makeQuoted( ( *i ).second.c_str() ) << "\n";
151         }
152 }
153
154 CGameDescription *g_pGameDescription; ///< shortcut to g_GamesDialog.m_pCurrentDescription
155
156
157 #include "warnings.h"
158 #include "stream/textfilestream.h"
159 #include "container/array.h"
160 #include "xml/ixml.h"
161 #include "xml/xmlparser.h"
162 #include "xml/xmlwriter.h"
163
164 #include "preferencedictionary.h"
165 #include "stringio.h"
166
167 const char* const PREFERENCES_VERSION = "1.0";
168
169 bool Preferences_Load( PreferenceDictionary& preferences, const char* filename, const char *cmdline_prefix ){
170         bool ret = false;
171         TextFileInputStream file( filename );
172         if ( !file.failed() ) {
173                 XMLStreamParser parser( file );
174                 XMLPreferenceDictionaryImporter importer( preferences, PREFERENCES_VERSION );
175                 parser.exportXML( importer );
176                 ret = true;
177         }
178
179         int l = strlen( cmdline_prefix );
180         for ( int i = 1; i < g_argc - 1; ++i )
181         {
182                 if ( g_argv[i][0] == '-' ) {
183                         if ( !strncmp( g_argv[i] + 1, cmdline_prefix, l ) ) {
184                                 if ( g_argv[i][l + 1] == '-' ) {
185                                         preferences.importPref( g_argv[i] + l + 2, g_argv[i + 1] );
186                                 }
187                         }
188                         ++i;
189                 }
190         }
191
192         return ret;
193 }
194
195 bool Preferences_Save( PreferenceDictionary& preferences, const char* filename ){
196         TextFileOutputStream file( filename );
197         if ( !file.failed() ) {
198                 XMLStreamWriter writer( file );
199                 XMLPreferenceDictionaryExporter exporter( preferences, PREFERENCES_VERSION );
200                 exporter.exportXML( writer );
201                 return true;
202         }
203         return false;
204 }
205
206 bool Preferences_Save_Safe( PreferenceDictionary& preferences, const char* filename ){
207         std::string tmpName( filename );
208         tmpName += "TMP";
209
210         return Preferences_Save( preferences, tmpName.c_str() )
211                    && ( !file_exists( filename ) || file_remove( filename ) )
212                    && file_move( tmpName.data(), filename );
213 }
214
215
216 struct LogConsole {
217         static void Export(const Callback<void(bool)> &returnz) {
218                 returnz(g_Console_enableLogging);
219         }
220
221         static void Import(bool value) {
222                 g_Console_enableLogging = value;
223                 Sys_EnableLogFile(g_Console_enableLogging);
224         }
225 };
226
227
228 void RegisterGlobalPreferences( PreferenceSystem& preferences ){
229         preferences.registerPreference( "gamefile", make_property_string( g_GamesDialog.m_sGameFile ) );
230         preferences.registerPreference( "gamePrompt", make_property_string( g_GamesDialog.m_bGamePrompt ) );
231         preferences.registerPreference( "skipGamePromptOnce", make_property_string( g_GamesDialog.m_bSkipGamePromptOnce ) );
232         preferences.registerPreference( "log console", make_property_string<LogConsole>() );
233 }
234
235
236 PreferenceDictionary g_global_preferences;
237
238 void GlobalPreferences_Init(){
239         RegisterGlobalPreferences( g_global_preferences );
240 }
241
242 void CGameDialog::LoadPrefs(){
243         // load global .pref file
244         StringOutputStream strGlobalPref( 256 );
245         strGlobalPref << g_Preferences.m_global_rc_path->str << "global.pref";
246
247         globalOutputStream() << "loading global preferences from " << makeQuoted( strGlobalPref.c_str() ) << "\n";
248
249         if ( !Preferences_Load( g_global_preferences, strGlobalPref.c_str(), "global" ) ) {
250                 globalOutputStream() << "failed to load global preferences from " << strGlobalPref.c_str() << "\n";
251         }
252 }
253
254 void CGameDialog::SavePrefs(){
255         StringOutputStream strGlobalPref( 256 );
256         strGlobalPref << g_Preferences.m_global_rc_path->str << "global.pref";
257
258         globalOutputStream() << "saving global preferences to " << strGlobalPref.c_str() << "\n";
259
260         if ( !Preferences_Save_Safe( g_global_preferences, strGlobalPref.c_str() ) ) {
261                 globalOutputStream() << "failed to save global preferences to " << strGlobalPref.c_str() << "\n";
262         }
263 }
264
265 void CGameDialog::DoGameDialog(){
266         // show the UI
267         DoModal();
268
269         // we save the prefs file
270         SavePrefs();
271 }
272
273 void CGameDialog::GameFileImport( int value ){
274         m_nComboSelect = value;
275         // use value to set m_sGameFile
276         std::list<CGameDescription *>::iterator iGame = mGames.begin();
277         int i;
278         for ( i = 0; i < value; i++ )
279         {
280                 ++iGame;
281         }
282
283         if ( ( *iGame )->mGameFile != m_sGameFile ) {
284                 m_sGameFile = ( *iGame )->mGameFile;
285
286                 // do not trigger radiant restart when switching game on startup using Global Preferences dialog
287                 if ( !onStartup ) {
288                         PreferencesDialog_restartRequired( "Selected Game" );
289                 }
290         }
291
292         // onStartup can only be true once, when Global Preferences are displayed at startup
293         onStartup = false;
294 }
295
296 void CGameDialog::GameFileExport( const Callback<void(int)> & importCallback ) const {
297         // use m_sGameFile to set value
298         std::list<CGameDescription *>::const_iterator iGame;
299         int i = 0;
300         for ( iGame = mGames.begin(); iGame != mGames.end(); ++iGame )
301         {
302                 if ( ( *iGame )->mGameFile == m_sGameFile ) {
303                         m_nComboSelect = i;
304                         break;
305                 }
306                 i++;
307         }
308         importCallback( m_nComboSelect );
309 }
310
311 struct CGameDialog_GameFile {
312         static void Export(const CGameDialog &self, const Callback<void(int)> &returnz) {
313                 self.GameFileExport(returnz);
314         }
315
316         static void Import(CGameDialog &self, int value) {
317                 self.GameFileImport(value);
318         }
319 };
320
321 void CGameDialog::CreateGlobalFrame( PreferencesPage& page ){
322         std::vector<const char*> games;
323         games.reserve( mGames.size() );
324         for ( std::list<CGameDescription *>::iterator i = mGames.begin(); i != mGames.end(); ++i )
325         {
326                 games.push_back( ( *i )->getRequiredKeyValue( "name" ) );
327         }
328         page.appendCombo(
329                 "Select the game",
330                 StringArrayRange( &( *games.begin() ), &( *games.end() ) ),
331                 make_property<CGameDialog_GameFile>(*this)
332                 );
333         page.appendCheckBox( "Startup", "Show Global Preferences", m_bGamePrompt );
334 }
335
336 ui::Window CGameDialog::BuildDialog(){
337         auto frame = create_dialog_frame( "Game settings", ui::Shadow::ETCHED_IN );
338
339         auto vbox2 = create_dialog_vbox( 0, 4 );
340         frame.add(vbox2);
341
342         {
343                 PreferencesPage preferencesPage( *this, vbox2 );
344                 Global_constructPreferences( preferencesPage );
345                 CreateGlobalFrame( preferencesPage );
346         }
347
348         return create_simple_modal_dialog_window( "Global Preferences", m_modal, frame );
349 }
350
351 static void StringReplace( std::string& input, const std::string& first, const std::string& second )
352 {
353         size_t found = 0;
354         while ( ( found = input.find(first, found) ) != std::string::npos )
355         {
356                 input.replace( found, first.length(), second );
357         }
358 }
359
360 // FIXME, for some unknown reason it sorts “Quake 3” after “Quake 4”.
361 static bool CompareGameName( CGameDescription *first, CGameDescription *second )
362 {
363         std::string string1( first->getRequiredKeyValue( "name" ) );
364         std::string string2( second->getRequiredKeyValue( "name" ) );
365
366         // HACK: Replace some roman numerals.
367         StringReplace( string1, " III", " 3" );
368         StringReplace( string2, " III", " 3" );
369         StringReplace( string1, " II", " 2" );
370         StringReplace( string2, " II", " 2" );
371
372         return string1 < string2;
373 }
374
375 void CGameDialog::ScanForGames(){
376         StringOutputStream strGamesPath( 256 );
377         strGamesPath << DataPath_get() << "gamepacks/games/";
378         const char *path = strGamesPath.c_str();
379
380         globalOutputStream() << "Scanning for game description files: " << path << '\n';
381
382         /*!
383            \todo FIXME LINUX:
384            do we put game description files below AppPath, or in ~/.radiant
385            i.e. read only or read/write?
386            my guess .. readonly cause it's an install
387            we will probably want to add ~/.radiant/<version>/games/ scanning on top of that for developers
388            (if that's really needed)
389          */
390
391         Directory_forEach(path, [&](const char *name) {
392                 if (!extension_equal(path_get_extension(name), "game")) {
393                         return;
394                 }
395                 StringOutputStream strPath(256);
396                 strPath << path << name;
397                 globalOutputStream() << strPath.c_str() << '\n';
398
399                 xmlDocPtr pDoc = xmlParseFile(strPath.c_str());
400                 if (pDoc) {
401                         mGames.push_front(new CGameDescription(pDoc, name));
402                         xmlFreeDoc(pDoc);
403                 } else {
404                         globalErrorStream() << "XML parser failed on '" << strPath.c_str() << "'\n";
405                 }
406
407                 mGames.sort(CompareGameName);
408         });
409 }
410
411 CGameDescription* CGameDialog::GameDescriptionForComboItem(){
412         std::list<CGameDescription *>::iterator iGame;
413         int i = 0;
414         for ( iGame = mGames.begin(); iGame != mGames.end(); ++iGame,i++ )
415         {
416                 if ( i == m_nComboSelect ) {
417                         return ( *iGame );
418                 }
419         }
420         return 0; // not found
421 }
422
423 void CGameDialog::InitGlobalPrefPath(){
424         g_Preferences.m_global_rc_path = g_string_new( SettingsPath_get() );
425 }
426
427 void CGameDialog::Reset(){
428         if ( !g_Preferences.m_global_rc_path ) {
429                 InitGlobalPrefPath();
430         }
431         StringOutputStream strGlobalPref( 256 );
432         strGlobalPref << g_Preferences.m_global_rc_path->str << "global.pref";
433         file_remove( strGlobalPref.c_str() );
434 }
435
436 void CGameDialog::Init(){
437         bool gamePrompt = false;
438
439         InitGlobalPrefPath();
440         LoadPrefs();
441         ScanForGames();
442
443         if ( mGames.empty() ) {
444                 Error( "Didn't find any valid game file descriptions, aborting\n" );
445         }
446         else
447         {
448                 std::list<CGameDescription *>::iterator iGame, iPrevGame;
449                 for ( iGame = mGames.begin(), iPrevGame = mGames.end(); iGame != mGames.end(); iPrevGame = iGame, ++iGame )
450                 {
451                         if ( iPrevGame != mGames.end() ) {
452                                 if ( strcmp( ( *iGame )->getRequiredKeyValue( "name" ), ( *iPrevGame )->getRequiredKeyValue( "name" ) ) < 0 ) {
453                                         CGameDescription *h = *iGame;
454                                         *iGame = *iPrevGame;
455                                         *iPrevGame = h;
456                                 }
457                         }
458                 }
459         }
460
461         CGameDescription* currentGameDescription = 0;
462
463         // m_bSkipGamePromptOnce is used to not prompt for game on restart, only on fresh startup
464         if ( m_bGamePrompt && !m_bSkipGamePromptOnce ) {
465                 gamePrompt = true;
466         }
467
468         m_bSkipGamePromptOnce = false;
469         g_GamesDialog.SavePrefs();
470
471         if ( !gamePrompt ) {
472                 // search by .game name
473                 std::list<CGameDescription *>::iterator iGame;
474                 for ( iGame = mGames.begin(); iGame != mGames.end(); ++iGame )
475                 {
476                         if ( ( *iGame )->mGameFile == m_sGameFile ) {
477                                 currentGameDescription = ( *iGame );
478                                 break;
479                         }
480                 }
481         }
482
483         if ( gamePrompt || !currentGameDescription ) {
484                 onStartup = true;
485                 Create();
486                 DoGameDialog();
487                 // use m_nComboSelect to identify the game to run as and set the globals
488                 currentGameDescription = GameDescriptionForComboItem();
489                 ASSERT_NOTNULL( currentGameDescription );
490         }
491         else {
492                 onStartup = false;
493         }
494
495         g_pGameDescription = currentGameDescription;
496
497         g_pGameDescription->Dump();
498 }
499
500 CGameDialog::~CGameDialog(){
501         // free all the game descriptions
502         std::list<CGameDescription *>::iterator iGame;
503         for ( iGame = mGames.begin(); iGame != mGames.end(); ++iGame )
504         {
505                 delete ( *iGame );
506                 *iGame = 0;
507         }
508         if ( GetWidget() ) {
509                 Destroy();
510         }
511 }
512
513 inline const char* GameDescription_getIdentifier( const CGameDescription& gameDescription ){
514         const char* identifier = gameDescription.getKeyValue( "index" );
515         if ( string_empty( identifier ) ) {
516                 identifier = "1";
517         }
518         return identifier;
519 }
520
521
522 CGameDialog g_GamesDialog;
523
524
525 // =============================================================================
526 // Widget callbacks for PrefsDlg
527
528 static void OnButtonClean( ui::Widget widget, gpointer data ){
529         // make sure this is what the user wants
530         if ( ui::alert( g_Preferences.GetWidget(), "This will close " RADIANT_NAME " and clean the corresponding registry entries.\n"
531                                                                                                                                   "Next time you start " RADIANT_NAME " it will be good as new. Do you wish to continue?",
532                                                  "Reset Registry", ui::alert_type::YESNO, ui::alert_icon::Asterisk ) == ui::alert_response::YES ) {
533                 PrefsDlg *dlg = (PrefsDlg*)data;
534                 dlg->EndModal( eIDCANCEL );
535
536                 g_preferences_globals.disable_ini = true;
537                 Preferences_Reset();
538                 gtk_main_quit();
539         }
540 }
541
542 // =============================================================================
543 // PrefsDlg class
544
545 /*
546    ========
547
548    very first prefs init deals with selecting the game and the game tools path
549    then we can load .ini stuff
550
551    using prefs / ini settings:
552    those are per-game
553
554    look in ~/.radiant/<version>/gamename
555    ========
556  */
557
558 const char *PREFS_LOCAL_FILENAME = "local.pref";
559
560 void PrefsDlg::Init(){
561         // m_global_rc_path has been set above
562         // m_rc_path is for game specific preferences
563         // takes the form: global-pref-path/gamename/prefs-file
564
565         // this is common to win32 and Linux init now
566         m_rc_path = g_string_new( m_global_rc_path->str );
567
568         // game sub-dir
569         g_string_append( m_rc_path, g_pGameDescription->mGameFile.c_str() );
570         g_string_append( m_rc_path, "/" );
571         Q_mkdir( m_rc_path->str );
572
573         // then the ini file
574         m_inipath = g_string_new( m_rc_path->str );
575         g_string_append( m_inipath, PREFS_LOCAL_FILENAME );
576 }
577
578 void notebook_set_page( ui::Widget notebook, ui::Widget page ){
579         int pagenum = gtk_notebook_page_num( GTK_NOTEBOOK( notebook ), page );
580         if ( gtk_notebook_get_current_page( GTK_NOTEBOOK( notebook ) ) != pagenum ) {
581                 gtk_notebook_set_current_page( GTK_NOTEBOOK( notebook ), pagenum );
582         }
583 }
584
585 void PrefsDlg::showPrefPage( ui::Widget prefpage ){
586         notebook_set_page( m_notebook, prefpage );
587         return;
588 }
589
590 static void treeSelection( ui::TreeSelection selection, gpointer data ){
591         PrefsDlg *dlg = (PrefsDlg*)data;
592
593         GtkTreeModel* model;
594         GtkTreeIter selected;
595         if ( gtk_tree_selection_get_selected( selection, &model, &selected ) ) {
596                 ui::Widget prefpage{ui::null};
597                 gtk_tree_model_get( model, &selected, 1, (gpointer*)&prefpage, -1 );
598                 dlg->showPrefPage( prefpage );
599         }
600 }
601
602 typedef std::list<PreferenceGroupCallback> PreferenceGroupCallbacks;
603
604 inline void PreferenceGroupCallbacks_constructGroup( const PreferenceGroupCallbacks& callbacks, PreferenceGroup& group ){
605         for ( PreferenceGroupCallbacks::const_iterator i = callbacks.begin(); i != callbacks.end(); ++i )
606         {
607                 ( *i )( group );
608         }
609 }
610
611
612 inline void PreferenceGroupCallbacks_pushBack( PreferenceGroupCallbacks& callbacks, const PreferenceGroupCallback& callback ){
613         callbacks.push_back( callback );
614 }
615
616 typedef std::list<PreferencesPageCallback> PreferencesPageCallbacks;
617
618 inline void PreferencesPageCallbacks_constructPage( const PreferencesPageCallbacks& callbacks, PreferencesPage& page ){
619         for ( PreferencesPageCallbacks::const_iterator i = callbacks.begin(); i != callbacks.end(); ++i )
620         {
621                 ( *i )( page );
622         }
623 }
624
625 inline void PreferencesPageCallbacks_pushBack( PreferencesPageCallbacks& callbacks, const PreferencesPageCallback& callback ){
626         callbacks.push_back( callback );
627 }
628
629 PreferencesPageCallbacks g_interfacePreferences;
630 void PreferencesDialog_addInterfacePreferences( const PreferencesPageCallback& callback ){
631         PreferencesPageCallbacks_pushBack( g_interfacePreferences, callback );
632 }
633 PreferenceGroupCallbacks g_interfaceCallbacks;
634 void PreferencesDialog_addInterfacePage( const PreferenceGroupCallback& callback ){
635         PreferenceGroupCallbacks_pushBack( g_interfaceCallbacks, callback );
636 }
637
638 PreferencesPageCallbacks g_displayPreferences;
639 void PreferencesDialog_addDisplayPreferences( const PreferencesPageCallback& callback ){
640         PreferencesPageCallbacks_pushBack( g_displayPreferences, callback );
641 }
642 PreferenceGroupCallbacks g_displayCallbacks;
643 void PreferencesDialog_addDisplayPage( const PreferenceGroupCallback& callback ){
644         PreferenceGroupCallbacks_pushBack( g_displayCallbacks, callback );
645 }
646
647 PreferencesPageCallbacks g_settingsPreferences;
648 void PreferencesDialog_addSettingsPreferences( const PreferencesPageCallback& callback ){
649         PreferencesPageCallbacks_pushBack( g_settingsPreferences, callback );
650 }
651 PreferenceGroupCallbacks g_settingsCallbacks;
652 void PreferencesDialog_addSettingsPage( const PreferenceGroupCallback& callback ){
653         PreferenceGroupCallbacks_pushBack( g_settingsCallbacks, callback );
654 }
655
656 void Widget_updateDependency( ui::Widget self, ui::Widget toggleButton ){
657         gtk_widget_set_sensitive( self, gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON( toggleButton ) ) && gtk_widget_is_sensitive( toggleButton ) );
658 }
659
660 void ToggleButton_toggled_Widget_updateDependency( ui::Widget toggleButton, ui::Widget self ){
661         Widget_updateDependency( self, toggleButton );
662 }
663
664 void ToggleButton_state_changed_Widget_updateDependency( ui::Widget toggleButton, GtkStateType state, ui::Widget self ){
665         if ( state == GTK_STATE_INSENSITIVE ) {
666                 Widget_updateDependency( self, toggleButton );
667         }
668 }
669
670 void Widget_connectToggleDependency( ui::Widget self, ui::Widget toggleButton ){
671         toggleButton.connect( "state_changed", G_CALLBACK( ToggleButton_state_changed_Widget_updateDependency ), self );
672         toggleButton.connect( "toggled", G_CALLBACK( ToggleButton_toggled_Widget_updateDependency ), self );
673         Widget_updateDependency( self, toggleButton );
674 }
675
676
677 inline ui::VBox getVBox( ui::Bin page ){
678         return ui::VBox::from(gtk_bin_get_child(page));
679 }
680
681 GtkTreeIter PreferenceTree_appendPage( ui::TreeStore store, GtkTreeIter* parent, const char* name, ui::Widget page ){
682         GtkTreeIter group;
683         gtk_tree_store_append( store, &group, parent );
684         gtk_tree_store_set( store, &group, 0, name, 1, page, -1 );
685         return group;
686 }
687
688 ui::Bin PreferencePages_addPage( ui::Widget notebook, const char* name ){
689         ui::Widget preflabel = ui::Label( name );
690         preflabel.show();
691
692         auto pageframe = ui::Frame( name );
693         gtk_container_set_border_width( GTK_CONTAINER( pageframe ), 4 );
694         pageframe.show();
695
696         ui::Widget vbox = ui::VBox( FALSE, 4 );
697         vbox.show();
698         gtk_container_set_border_width( GTK_CONTAINER( vbox ), 4 );
699         pageframe.add(vbox);
700
701         // Add the page to the notebook
702         gtk_notebook_append_page( GTK_NOTEBOOK( notebook ), pageframe, preflabel );
703
704         return pageframe;
705 }
706
707 class PreferenceTreeGroup : public PreferenceGroup
708 {
709 Dialog& m_dialog;
710 ui::Widget m_notebook;
711 ui::TreeStore m_store;
712 GtkTreeIter m_group;
713 public:
714 PreferenceTreeGroup( Dialog& dialog, ui::Widget notebook, ui::TreeStore store, GtkTreeIter group ) :
715         m_dialog( dialog ),
716         m_notebook( notebook ),
717         m_store( store ),
718         m_group( group ){
719 }
720 PreferencesPage createPage( const char* treeName, const char* frameName ){
721         auto page = PreferencePages_addPage( m_notebook, frameName );
722         PreferenceTree_appendPage( m_store, &m_group, treeName, page );
723         return PreferencesPage( m_dialog, getVBox( page ) );
724 }
725 };
726
727 ui::Window PrefsDlg::BuildDialog(){
728         PreferencesDialog_addInterfacePreferences( makeCallbackF(Interface_constructPreferences) );
729         Mouse_registerPreferencesPage();
730
731         ui::Window dialog = ui::Window(create_floating_window( RADIANT_NAME " Preferences", m_parent ));
732
733         gtk_window_set_transient_for( dialog, m_parent );
734         gtk_window_set_position( dialog, GTK_WIN_POS_CENTER_ON_PARENT );
735
736         {
737                 auto mainvbox = ui::VBox( FALSE, 5 );
738                 dialog.add(mainvbox);
739                 gtk_container_set_border_width( GTK_CONTAINER( mainvbox ), 5 );
740                 mainvbox.show();
741
742                 {
743                         auto hbox = ui::HBox( FALSE, 5 );
744                         hbox.show();
745                         mainvbox.pack_end(hbox, FALSE, TRUE, 0);
746
747                         {
748                                 auto button = create_dialog_button( "OK", G_CALLBACK( dialog_button_ok ), &m_modal );
749                                 hbox.pack_end(button, FALSE, FALSE, 0);
750                         }
751                         {
752                                 auto button = create_dialog_button( "Cancel", G_CALLBACK( dialog_button_cancel ), &m_modal );
753                                 hbox.pack_end(button, FALSE, FALSE, 0);
754                         }
755                         {
756                                 auto button = create_dialog_button( "Clean", G_CALLBACK( OnButtonClean ), this );
757                                 hbox.pack_end(button, FALSE, FALSE, 0);
758                         }
759                 }
760
761                 {
762                         auto hbox = ui::HBox( FALSE, 5 );
763                         mainvbox.pack_start( hbox, TRUE, TRUE, 0 );
764                         hbox.show();
765
766                         {
767                                 auto sc_win = ui::ScrolledWindow(ui::New);
768                                 gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( sc_win ), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC );
769                                 hbox.pack_start( sc_win, FALSE, FALSE, 0 );
770                                 sc_win.show();
771                                 gtk_scrolled_window_set_shadow_type( GTK_SCROLLED_WINDOW( sc_win ), GTK_SHADOW_IN );
772
773                                 // prefs pages notebook
774                                 m_notebook = ui::Widget::from(gtk_notebook_new());
775                                 // hide the notebook tabs since its not supposed to look like a notebook
776                                 gtk_notebook_set_show_tabs( GTK_NOTEBOOK( m_notebook ), FALSE );
777                                 hbox.pack_start( m_notebook, TRUE, TRUE, 0 );
778                                 m_notebook.show();
779
780
781                                 {
782                                         auto store = ui::TreeStore::from(gtk_tree_store_new( 2, G_TYPE_STRING, G_TYPE_POINTER ));
783
784                                         auto view = ui::TreeView(ui::TreeModel::from(store._handle));
785                                         gtk_tree_view_set_headers_visible(view, FALSE );
786
787                                         {
788                                                 auto renderer = ui::CellRendererText(ui::New);
789                         auto column = ui::TreeViewColumn( "Preferences", renderer, {{"text", 0}} );
790                                                 gtk_tree_view_append_column(view, column );
791                                         }
792
793                                         {
794                                                 auto selection = ui::TreeSelection::from(gtk_tree_view_get_selection(view));
795                                                 selection.connect( "changed", G_CALLBACK( treeSelection ), this );
796                                         }
797
798                                         view.show();
799
800                                         sc_win.add(view);
801
802                                         {
803                                                 /********************************************************************/
804                                                 /* Add preference tree options                                      */
805                                                 /********************************************************************/
806                                                 // Front page...
807                                                 //GtkWidget* front =
808                                                 PreferencePages_addPage( m_notebook, "Front Page" );
809
810                                                 {
811                                                         auto global = PreferencePages_addPage( m_notebook, "Global Preferences" );
812                                                         {
813                                                                 PreferencesPage preferencesPage( *this, getVBox( global ) );
814                                                                 Global_constructPreferences( preferencesPage );
815                                                         }
816                             auto group = PreferenceTree_appendPage( store, 0, "Global", global );
817                                                         {
818                                                                 auto game = PreferencePages_addPage( m_notebook, "Game" );
819                                                                 PreferencesPage preferencesPage( *this, getVBox( game ) );
820                                                                 g_GamesDialog.CreateGlobalFrame( preferencesPage );
821
822                                                                 PreferenceTree_appendPage( store, &group, "Game", game );
823                                                         }
824                                                 }
825
826                                                 {
827                                                         auto interfacePage = PreferencePages_addPage( m_notebook, "Interface Preferences" );
828                                                         {
829                                                                 PreferencesPage preferencesPage( *this, getVBox( interfacePage ) );
830                                                                 PreferencesPageCallbacks_constructPage( g_interfacePreferences, preferencesPage );
831                                                         }
832
833                             auto group = PreferenceTree_appendPage( store, 0, "Interface", interfacePage );
834                                                         PreferenceTreeGroup preferenceGroup( *this, m_notebook, store, group );
835
836                                                         PreferenceGroupCallbacks_constructGroup( g_interfaceCallbacks, preferenceGroup );
837                                                 }
838
839                                                 {
840                                                         auto display = PreferencePages_addPage( m_notebook, "Display Preferences" );
841                                                         {
842                                                                 PreferencesPage preferencesPage( *this, getVBox( display ) );
843                                                                 PreferencesPageCallbacks_constructPage( g_displayPreferences, preferencesPage );
844                                                         }
845                             auto group = PreferenceTree_appendPage( store, 0, "Display", display );
846                                                         PreferenceTreeGroup preferenceGroup( *this, m_notebook, store, group );
847
848                                                         PreferenceGroupCallbacks_constructGroup( g_displayCallbacks, preferenceGroup );
849                                                 }
850
851                                                 {
852                                                         auto settings = PreferencePages_addPage( m_notebook, "General Settings" );
853                                                         {
854                                                                 PreferencesPage preferencesPage( *this, getVBox( settings ) );
855                                                                 PreferencesPageCallbacks_constructPage( g_settingsPreferences, preferencesPage );
856                                                         }
857
858                             auto group = PreferenceTree_appendPage( store, 0, "Settings", settings );
859                                                         PreferenceTreeGroup preferenceGroup( *this, m_notebook, store, group );
860
861                                                         PreferenceGroupCallbacks_constructGroup( g_settingsCallbacks, preferenceGroup );
862                                                 }
863                                         }
864
865                                         gtk_tree_view_expand_all(view );
866
867                                         g_object_unref( G_OBJECT( store ) );
868                                 }
869                         }
870                 }
871         }
872
873         gtk_notebook_set_current_page( GTK_NOTEBOOK( m_notebook ), 0 );
874
875         return dialog;
876 }
877
878 preferences_globals_t g_preferences_globals;
879
880 PrefsDlg g_Preferences;               // global prefs instance
881
882
883 void PreferencesDialog_constructWindow( ui::Window main_window ){
884         g_Preferences.m_parent = main_window;
885         g_Preferences.Create();
886 }
887 void PreferencesDialog_destroyWindow(){
888         g_Preferences.Destroy();
889 }
890
891
892 PreferenceDictionary g_preferences;
893
894 PreferenceSystem& GetPreferenceSystem(){
895         return g_preferences;
896 }
897
898 class PreferenceSystemAPI
899 {
900 PreferenceSystem* m_preferencesystem;
901 public:
902 typedef PreferenceSystem Type;
903 STRING_CONSTANT( Name, "*" );
904
905 PreferenceSystemAPI(){
906         m_preferencesystem = &GetPreferenceSystem();
907 }
908 PreferenceSystem* getTable(){
909         return m_preferencesystem;
910 }
911 };
912
913 #include "modulesystem/singletonmodule.h"
914 #include "modulesystem/moduleregistry.h"
915
916 typedef SingletonModule<PreferenceSystemAPI> PreferenceSystemModule;
917 typedef Static<PreferenceSystemModule> StaticPreferenceSystemModule;
918 StaticRegisterModule staticRegisterPreferenceSystem( StaticPreferenceSystemModule::instance() );
919
920 void Preferences_Load(){
921         g_GamesDialog.LoadPrefs();
922
923         globalOutputStream() << "loading local preferences from " << g_Preferences.m_inipath->str << "\n";
924
925         if ( !Preferences_Load( g_preferences, g_Preferences.m_inipath->str, g_GamesDialog.m_sGameFile.c_str() ) ) {
926                 globalOutputStream() << "failed to load local preferences from " << g_Preferences.m_inipath->str << "\n";
927         }
928 }
929
930 void Preferences_Save(){
931         if ( g_preferences_globals.disable_ini ) {
932                 return;
933         }
934
935         // save global preferences
936         g_GamesDialog.SavePrefs();
937
938         globalOutputStream() << "saving local preferences to " << g_Preferences.m_inipath->str << "\n";
939
940         if ( !Preferences_Save_Safe( g_preferences, g_Preferences.m_inipath->str ) ) {
941                 globalOutputStream() << "failed to save local preferences to " << g_Preferences.m_inipath->str << "\n";
942         }
943 }
944
945 void Preferences_Reset(){
946         file_remove( g_Preferences.m_inipath->str );
947 }
948
949
950 void PrefsDlg::PostModal( EMessageBoxReturn code ){
951         if ( code == eIDOK ) {
952                 Preferences_Save();
953                 UpdateAllWindows();
954         }
955 }
956
957 std::vector<const char*> g_restart_required;
958
959 void PreferencesDialog_restartRequired( const char* staticName ){
960         g_restart_required.push_back( staticName );
961 }
962
963 bool PreferencesDialog_isRestartRequired(){
964         return !g_restart_required.empty();
965 }
966
967 void PreferencesDialog_restartIfRequired(){
968         if ( !g_restart_required.empty() ) {
969                 StringOutputStream message( 256 );
970                 message << "Preference changes require a restart:\n\n";
971
972                 for ( std::vector<const char*>::iterator i = g_restart_required.begin(); i != g_restart_required.end(); ++i )
973                 {
974                         message << ( *i ) << '\n';
975                 }
976
977                 message << "\nRestart now?";
978
979                 auto ret = ui::alert( MainFrame_getWindow(), message.c_str(), "Restart " RADIANT_NAME "?", ui::alert_type::YESNO, ui::alert_icon::Question );
980
981                 g_restart_required.clear();
982
983                 if ( ret == ui::alert_response::YES ) {
984                         g_GamesDialog.m_bSkipGamePromptOnce = true;
985                         Radiant_Restart();
986                 }
987         }
988 }
989
990 void PreferencesDialog_showDialog(){
991         if ( ConfirmModified( "Edit Preferences" ) && g_Preferences.DoModal() == eIDOK ) {
992                 PreferencesDialog_restartIfRequired();
993         }
994 }
995
996 struct GameName {
997         static void Export(const Callback<void(const char *)> &returnz) {
998                 returnz(gamename_get());
999         }
1000
1001         static void Import(const char *value) {
1002                 gamename_set(value);
1003         }
1004 };
1005
1006 struct GameMode {
1007         static void Export(const Callback<void(const char *)> &returnz) {
1008                 returnz(gamemode_get());
1009         }
1010
1011         static void Import(const char *value) {
1012                 gamemode_set(value);
1013         }
1014 };
1015
1016 void RegisterPreferences( PreferenceSystem& preferences ){
1017 #if GDEF_OS_WINDOWS
1018         preferences.registerPreference( "UseCustomShaderEditor", make_property_string( g_TextEditor_useWin32Editor ) );
1019 #else
1020         preferences.registerPreference( "UseCustomShaderEditor", make_property_string( g_TextEditor_useCustomEditor ) );
1021         preferences.registerPreference( "CustomShaderEditorCommand", make_property_string( g_TextEditor_editorCommand ) );
1022 #endif
1023
1024         preferences.registerPreference( "GameName", make_property<GameName>() );
1025         preferences.registerPreference( "GameMode", make_property<GameMode>() );
1026 }
1027
1028 void Preferences_Init(){
1029         RegisterPreferences( GetPreferenceSystem() );
1030 }