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