510a1cc102852c81c7ef60ed76b1892afafeb63a
[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 << AppPath_get() << 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         Array<char> tmpName( filename, filename + strlen( filename ) + 1 + 3 );
208         *( tmpName.end() - 4 ) = 'T';
209         *( tmpName.end() - 3 ) = 'M';
210         *( tmpName.end() - 2 ) = 'P';
211         *( tmpName.end() - 1 ) = '\0';
212
213         return Preferences_Save( preferences, tmpName.data() )
214                    && ( !file_exists( filename ) || file_remove( filename ) )
215                    && file_move( tmpName.data(), filename );
216 }
217
218
219
220 void LogConsole_importString( const char* string ){
221         g_Console_enableLogging = string_equal( string, "true" );
222         Sys_LogFile( g_Console_enableLogging );
223 }
224 typedef FreeCaller<void(const char*), LogConsole_importString> LogConsoleImportStringCaller;
225
226
227 void RegisterGlobalPreferences( PreferenceSystem& preferences ){
228         preferences.registerPreference( "gamefile", CopiedStringImportStringCaller( g_GamesDialog.m_sGameFile ), CopiedStringExportStringCaller( g_GamesDialog.m_sGameFile ) );
229         preferences.registerPreference( "gamePrompt", BoolImportStringCaller( g_GamesDialog.m_bGamePrompt ), BoolExportStringCaller( g_GamesDialog.m_bGamePrompt ) );
230         preferences.registerPreference( "log console", LogConsoleImportStringCaller(), BoolExportStringCaller( g_Console_enableLogging ) );
231 }
232
233
234 PreferenceDictionary g_global_preferences;
235
236 void GlobalPreferences_Init(){
237         RegisterGlobalPreferences( g_global_preferences );
238 }
239
240 void CGameDialog::LoadPrefs(){
241         // load global .pref file
242         StringOutputStream strGlobalPref( 256 );
243         strGlobalPref << g_Preferences.m_global_rc_path->str << "global.pref";
244
245         globalOutputStream() << "loading global preferences from " << makeQuoted( strGlobalPref.c_str() ) << "\n";
246
247         if ( !Preferences_Load( g_global_preferences, strGlobalPref.c_str(), "global" ) ) {
248                 globalOutputStream() << "failed to load global preferences from " << strGlobalPref.c_str() << "\n";
249         }
250 }
251
252 void CGameDialog::SavePrefs(){
253         StringOutputStream strGlobalPref( 256 );
254         strGlobalPref << g_Preferences.m_global_rc_path->str << "global.pref";
255
256         globalOutputStream() << "saving global preferences to " << strGlobalPref.c_str() << "\n";
257
258         if ( !Preferences_Save_Safe( g_global_preferences, strGlobalPref.c_str() ) ) {
259                 globalOutputStream() << "failed to save global preferences to " << strGlobalPref.c_str() << "\n";
260         }
261 }
262
263 void CGameDialog::DoGameDialog(){
264         // show the UI
265         DoModal();
266
267         // we save the prefs file
268         SavePrefs();
269 }
270
271 void CGameDialog::GameFileImport( int value ){
272         m_nComboSelect = value;
273         // use value to set m_sGameFile
274         std::list<CGameDescription *>::iterator iGame = mGames.begin();
275         int i;
276         for ( i = 0; i < value; i++ )
277         {
278                 ++iGame;
279         }
280         m_sGameFile = ( *iGame )->mGameFile;
281 }
282
283 void CGameDialog::GameFileExport( const IntImportCallback& importCallback ) const {
284         // use m_sGameFile to set value
285         std::list<CGameDescription *>::const_iterator iGame;
286         int i = 0;
287         for ( iGame = mGames.begin(); iGame != mGames.end(); ++iGame )
288         {
289                 if ( ( *iGame )->mGameFile == m_sGameFile ) {
290                         m_nComboSelect = i;
291                         break;
292                 }
293                 i++;
294         }
295         importCallback( m_nComboSelect );
296 }
297
298 void CGameDialog_GameFileImport( CGameDialog& self, int value ){
299         self.GameFileImport( value );
300 }
301
302 void CGameDialog_GameFileExport( CGameDialog& self, const IntImportCallback& importCallback ){
303         self.GameFileExport( importCallback );
304 }
305
306 void CGameDialog::CreateGlobalFrame( PreferencesPage& page ){
307         std::vector<const char*> games;
308         games.reserve( mGames.size() );
309         for ( std::list<CGameDescription *>::iterator i = mGames.begin(); i != mGames.end(); ++i )
310         {
311                 games.push_back( ( *i )->getRequiredKeyValue( "name" ) );
312         }
313         page.appendCombo(
314                 "Select the game",
315                 StringArrayRange( &( *games.begin() ), &( *games.end() ) ),
316                 ReferenceCaller<CGameDialog, void(int), CGameDialog_GameFileImport>( *this ),
317                 ReferenceCaller<CGameDialog, void(const IntImportCallback&), CGameDialog_GameFileExport>( *this )
318                 );
319         page.appendCheckBox( "Startup", "Show Global Preferences", m_bGamePrompt );
320 }
321
322 ui::Window CGameDialog::BuildDialog(){
323         auto frame = create_dialog_frame( "Game settings", ui::Shadow::ETCHED_IN );
324
325         auto vbox2 = create_dialog_vbox( 0, 4 );
326         frame.add(vbox2);
327
328         {
329                 PreferencesPage preferencesPage( *this, vbox2 );
330                 Global_constructPreferences( preferencesPage );
331                 CreateGlobalFrame( preferencesPage );
332         }
333
334         return create_simple_modal_dialog_window( "Global Preferences", m_modal, frame );
335 }
336
337 void CGameDialog::ScanForGames(){
338         StringOutputStream strGamesPath( 256 );
339         strGamesPath << AppPath_get() << "games/";
340         const char *path = strGamesPath.c_str();
341
342         globalOutputStream() << "Scanning for game description files: " << path << '\n';
343
344         /*!
345            \todo FIXME LINUX:
346            do we put game description files below AppPath, or in ~/.radiant
347            i.e. read only or read/write?
348            my guess .. readonly cause it's an install
349            we will probably want to add ~/.radiant/<version>/games/ scanning on top of that for developers
350            (if that's really needed)
351          */
352
353         Directory_forEach(path, [&](const char *name) {
354                 if (!extension_equal(path_get_extension(name), "game")) {
355                         return;
356                 }
357                 StringOutputStream strPath(256);
358                 strPath << path << name;
359                 globalOutputStream() << strPath.c_str() << '\n';
360
361                 xmlDocPtr pDoc = xmlParseFile(strPath.c_str());
362                 if (pDoc) {
363                         mGames.push_front(new CGameDescription(pDoc, name));
364                         xmlFreeDoc(pDoc);
365                 } else {
366                         globalErrorStream() << "XML parser failed on '" << strPath.c_str() << "'\n";
367                 }
368         });
369 }
370
371 CGameDescription* CGameDialog::GameDescriptionForComboItem(){
372         std::list<CGameDescription *>::iterator iGame;
373         int i = 0;
374         for ( iGame = mGames.begin(); iGame != mGames.end(); ++iGame,i++ )
375         {
376                 if ( i == m_nComboSelect ) {
377                         return ( *iGame );
378                 }
379         }
380         return 0; // not found
381 }
382
383 void CGameDialog::InitGlobalPrefPath(){
384         g_Preferences.m_global_rc_path = g_string_new( SettingsPath_get() );
385 }
386
387 void CGameDialog::Reset(){
388         if ( !g_Preferences.m_global_rc_path ) {
389                 InitGlobalPrefPath();
390         }
391         StringOutputStream strGlobalPref( 256 );
392         strGlobalPref << g_Preferences.m_global_rc_path->str << "global.pref";
393         file_remove( strGlobalPref.c_str() );
394 }
395
396 void CGameDialog::Init(){
397         InitGlobalPrefPath();
398         LoadPrefs();
399         ScanForGames();
400         if ( mGames.empty() ) {
401                 Error( "Didn't find any valid game file descriptions, aborting\n" );
402         }
403         else
404         {
405                 std::list<CGameDescription *>::iterator iGame, iPrevGame;
406                 for ( iGame = mGames.begin(), iPrevGame = mGames.end(); iGame != mGames.end(); iPrevGame = iGame, ++iGame )
407                 {
408                         if ( iPrevGame != mGames.end() ) {
409                                 if ( strcmp( ( *iGame )->getRequiredKeyValue( "name" ), ( *iPrevGame )->getRequiredKeyValue( "name" ) ) < 0 ) {
410                                         CGameDescription *h = *iGame;
411                                         *iGame = *iPrevGame;
412                                         *iPrevGame = h;
413                                 }
414                         }
415                 }
416         }
417
418         CGameDescription* currentGameDescription = 0;
419
420         if ( !m_bGamePrompt ) {
421                 // search by .game name
422                 std::list<CGameDescription *>::iterator iGame;
423                 for ( iGame = mGames.begin(); iGame != mGames.end(); ++iGame )
424                 {
425                         if ( ( *iGame )->mGameFile == m_sGameFile ) {
426                                 currentGameDescription = ( *iGame );
427                                 break;
428                         }
429                 }
430         }
431         if ( m_bGamePrompt || !currentGameDescription ) {
432                 Create();
433                 DoGameDialog();
434                 // use m_nComboSelect to identify the game to run as and set the globals
435                 currentGameDescription = GameDescriptionForComboItem();
436                 ASSERT_NOTNULL( currentGameDescription );
437         }
438         g_pGameDescription = currentGameDescription;
439
440         g_pGameDescription->Dump();
441 }
442
443 CGameDialog::~CGameDialog(){
444         // free all the game descriptions
445         std::list<CGameDescription *>::iterator iGame;
446         for ( iGame = mGames.begin(); iGame != mGames.end(); ++iGame )
447         {
448                 delete ( *iGame );
449                 *iGame = 0;
450         }
451         if ( GetWidget() ) {
452                 Destroy();
453         }
454 }
455
456 inline const char* GameDescription_getIdentifier( const CGameDescription& gameDescription ){
457         const char* identifier = gameDescription.getKeyValue( "index" );
458         if ( string_empty( identifier ) ) {
459                 identifier = "1";
460         }
461         return identifier;
462 }
463
464
465 CGameDialog g_GamesDialog;
466
467
468 // =============================================================================
469 // Widget callbacks for PrefsDlg
470
471 static void OnButtonClean( ui::Widget widget, gpointer data ){
472         // make sure this is what the user wants
473         if ( g_Preferences.GetWidget().alert( "This will close Radiant and clean the corresponding registry entries.\n"
474                                                                                                                                   "Next time you start Radiant it will be good as new. Do you wish to continue?",
475                                                  "Reset Registry", ui::alert_type::YESNO, ui::alert_icon::Asterisk ) == ui::alert_response::YES ) {
476                 PrefsDlg *dlg = (PrefsDlg*)data;
477                 dlg->EndModal( eIDCANCEL );
478
479                 g_preferences_globals.disable_ini = true;
480                 Preferences_Reset();
481                 gtk_main_quit();
482         }
483 }
484
485 // =============================================================================
486 // PrefsDlg class
487
488 /*
489    ========
490
491    very first prefs init deals with selecting the game and the game tools path
492    then we can load .ini stuff
493
494    using prefs / ini settings:
495    those are per-game
496
497    look in ~/.radiant/<version>/gamename
498    ========
499  */
500
501 const char *PREFS_LOCAL_FILENAME = "local.pref";
502
503 void PrefsDlg::Init(){
504         // m_global_rc_path has been set above
505         // m_rc_path is for game specific preferences
506         // takes the form: global-pref-path/gamename/prefs-file
507
508         // this is common to win32 and Linux init now
509         m_rc_path = g_string_new( m_global_rc_path->str );
510
511         // game sub-dir
512         g_string_append( m_rc_path, g_pGameDescription->mGameFile.c_str() );
513         g_string_append( m_rc_path, "/" );
514         Q_mkdir( m_rc_path->str );
515
516         // then the ini file
517         m_inipath = g_string_new( m_rc_path->str );
518         g_string_append( m_inipath, PREFS_LOCAL_FILENAME );
519 }
520
521 void notebook_set_page( ui::Widget notebook, ui::Widget page ){
522         int pagenum = gtk_notebook_page_num( GTK_NOTEBOOK( notebook ), page );
523         if ( gtk_notebook_get_current_page( GTK_NOTEBOOK( notebook ) ) != pagenum ) {
524                 gtk_notebook_set_current_page( GTK_NOTEBOOK( notebook ), pagenum );
525         }
526 }
527
528 void PrefsDlg::showPrefPage( ui::Widget prefpage ){
529         notebook_set_page( m_notebook, prefpage );
530         return;
531 }
532
533 static void treeSelection( ui::TreeSelection selection, gpointer data ){
534         PrefsDlg *dlg = (PrefsDlg*)data;
535
536         GtkTreeModel* model;
537         GtkTreeIter selected;
538         if ( gtk_tree_selection_get_selected( selection, &model, &selected ) ) {
539                 ui::Widget prefpage{ui::null};
540                 gtk_tree_model_get( model, &selected, 1, (gpointer*)&prefpage, -1 );
541                 dlg->showPrefPage( prefpage );
542         }
543 }
544
545 typedef std::list<PreferenceGroupCallback> PreferenceGroupCallbacks;
546
547 inline void PreferenceGroupCallbacks_constructGroup( const PreferenceGroupCallbacks& callbacks, PreferenceGroup& group ){
548         for ( PreferenceGroupCallbacks::const_iterator i = callbacks.begin(); i != callbacks.end(); ++i )
549         {
550                 ( *i )( group );
551         }
552 }
553
554
555 inline void PreferenceGroupCallbacks_pushBack( PreferenceGroupCallbacks& callbacks, const PreferenceGroupCallback& callback ){
556         callbacks.push_back( callback );
557 }
558
559 typedef std::list<PreferencesPageCallback> PreferencesPageCallbacks;
560
561 inline void PreferencesPageCallbacks_constructPage( const PreferencesPageCallbacks& callbacks, PreferencesPage& page ){
562         for ( PreferencesPageCallbacks::const_iterator i = callbacks.begin(); i != callbacks.end(); ++i )
563         {
564                 ( *i )( page );
565         }
566 }
567
568 inline void PreferencesPageCallbacks_pushBack( PreferencesPageCallbacks& callbacks, const PreferencesPageCallback& callback ){
569         callbacks.push_back( callback );
570 }
571
572 PreferencesPageCallbacks g_interfacePreferences;
573 void PreferencesDialog_addInterfacePreferences( const PreferencesPageCallback& callback ){
574         PreferencesPageCallbacks_pushBack( g_interfacePreferences, callback );
575 }
576 PreferenceGroupCallbacks g_interfaceCallbacks;
577 void PreferencesDialog_addInterfacePage( const PreferenceGroupCallback& callback ){
578         PreferenceGroupCallbacks_pushBack( g_interfaceCallbacks, callback );
579 }
580
581 PreferencesPageCallbacks g_displayPreferences;
582 void PreferencesDialog_addDisplayPreferences( const PreferencesPageCallback& callback ){
583         PreferencesPageCallbacks_pushBack( g_displayPreferences, callback );
584 }
585 PreferenceGroupCallbacks g_displayCallbacks;
586 void PreferencesDialog_addDisplayPage( const PreferenceGroupCallback& callback ){
587         PreferenceGroupCallbacks_pushBack( g_displayCallbacks, callback );
588 }
589
590 PreferencesPageCallbacks g_settingsPreferences;
591 void PreferencesDialog_addSettingsPreferences( const PreferencesPageCallback& callback ){
592         PreferencesPageCallbacks_pushBack( g_settingsPreferences, callback );
593 }
594 PreferenceGroupCallbacks g_settingsCallbacks;
595 void PreferencesDialog_addSettingsPage( const PreferenceGroupCallback& callback ){
596         PreferenceGroupCallbacks_pushBack( g_settingsCallbacks, callback );
597 }
598
599 void Widget_updateDependency( ui::Widget self, ui::Widget toggleButton ){
600         gtk_widget_set_sensitive( self, gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON( toggleButton ) ) && gtk_widget_is_sensitive( toggleButton ) );
601 }
602
603 void ToggleButton_toggled_Widget_updateDependency( ui::Widget toggleButton, ui::Widget self ){
604         Widget_updateDependency( self, toggleButton );
605 }
606
607 void ToggleButton_state_changed_Widget_updateDependency( ui::Widget toggleButton, GtkStateType state, ui::Widget self ){
608         if ( state == GTK_STATE_INSENSITIVE ) {
609                 Widget_updateDependency( self, toggleButton );
610         }
611 }
612
613 void Widget_connectToggleDependency( ui::Widget self, ui::Widget toggleButton ){
614         toggleButton.connect( "state_changed", G_CALLBACK( ToggleButton_state_changed_Widget_updateDependency ), self );
615         toggleButton.connect( "toggled", G_CALLBACK( ToggleButton_toggled_Widget_updateDependency ), self );
616         Widget_updateDependency( self, toggleButton );
617 }
618
619
620 inline ui::VBox getVBox( ui::Bin page ){
621         return ui::VBox::from(gtk_bin_get_child(page));
622 }
623
624 GtkTreeIter PreferenceTree_appendPage( ui::TreeStore store, GtkTreeIter* parent, const char* name, ui::Widget page ){
625         GtkTreeIter group;
626         gtk_tree_store_append( store, &group, parent );
627         gtk_tree_store_set( store, &group, 0, name, 1, page, -1 );
628         return group;
629 }
630
631 ui::Bin PreferencePages_addPage( ui::Widget notebook, const char* name ){
632         ui::Widget preflabel = ui::Label( name );
633         preflabel.show();
634
635         auto pageframe = ui::Frame( name );
636         gtk_container_set_border_width( GTK_CONTAINER( pageframe ), 4 );
637         pageframe.show();
638
639         ui::Widget vbox = ui::VBox( FALSE, 4 );
640         vbox.show();
641         gtk_container_set_border_width( GTK_CONTAINER( vbox ), 4 );
642         pageframe.add(vbox);
643
644         // Add the page to the notebook
645         gtk_notebook_append_page( GTK_NOTEBOOK( notebook ), pageframe, preflabel );
646
647         return pageframe;
648 }
649
650 class PreferenceTreeGroup : public PreferenceGroup
651 {
652 Dialog& m_dialog;
653 ui::Widget m_notebook;
654 ui::TreeStore m_store;
655 GtkTreeIter m_group;
656 public:
657 PreferenceTreeGroup( Dialog& dialog, ui::Widget notebook, ui::TreeStore store, GtkTreeIter group ) :
658         m_dialog( dialog ),
659         m_notebook( notebook ),
660         m_store( store ),
661         m_group( group ){
662 }
663 PreferencesPage createPage( const char* treeName, const char* frameName ){
664         auto page = PreferencePages_addPage( m_notebook, frameName );
665         PreferenceTree_appendPage( m_store, &m_group, treeName, page );
666         return PreferencesPage( m_dialog, getVBox( page ) );
667 }
668 };
669
670 ui::Window PrefsDlg::BuildDialog(){
671         PreferencesDialog_addInterfacePreferences( makeCallbackF(Interface_constructPreferences) );
672         Mouse_registerPreferencesPage();
673
674         ui::Window dialog = ui::Window(create_floating_window( "NetRadiant Preferences", m_parent ));
675
676         {
677                 auto mainvbox = ui::VBox( FALSE, 5 );
678                 dialog.add(mainvbox);
679                 gtk_container_set_border_width( GTK_CONTAINER( mainvbox ), 5 );
680                 mainvbox.show();
681
682                 {
683                         auto hbox = ui::HBox( FALSE, 5 );
684                         hbox.show();
685                         mainvbox.pack_end(hbox, FALSE, TRUE, 0);
686
687                         {
688                                 auto button = create_dialog_button( "OK", G_CALLBACK( dialog_button_ok ), &m_modal );
689                                 hbox.pack_end(button, FALSE, FALSE, 0);
690                         }
691                         {
692                                 auto button = create_dialog_button( "Cancel", G_CALLBACK( dialog_button_cancel ), &m_modal );
693                                 hbox.pack_end(button, FALSE, FALSE, 0);
694                         }
695                         {
696                                 auto button = create_dialog_button( "Clean", G_CALLBACK( OnButtonClean ), this );
697                                 hbox.pack_end(button, FALSE, FALSE, 0);
698                         }
699                 }
700
701                 {
702                         auto hbox = ui::HBox( FALSE, 5 );
703                         mainvbox.pack_start( hbox, TRUE, TRUE, 0 );
704                         hbox.show();
705
706                         {
707                                 auto sc_win = ui::ScrolledWindow(ui::New);
708                                 gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( sc_win ), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC );
709                                 hbox.pack_start( sc_win, FALSE, FALSE, 0 );
710                                 sc_win.show();
711                                 gtk_scrolled_window_set_shadow_type( GTK_SCROLLED_WINDOW( sc_win ), GTK_SHADOW_IN );
712
713                                 // prefs pages notebook
714                                 m_notebook = ui::Widget(gtk_notebook_new());
715                                 // hide the notebook tabs since its not supposed to look like a notebook
716                                 gtk_notebook_set_show_tabs( GTK_NOTEBOOK( m_notebook ), FALSE );
717                                 hbox.pack_start( m_notebook, TRUE, TRUE, 0 );
718                                 m_notebook.show();
719
720
721                                 {
722                                         auto store = ui::TreeStore(gtk_tree_store_new( 2, G_TYPE_STRING, G_TYPE_POINTER ));
723
724                                         auto view = ui::TreeView(ui::TreeModel(store));
725                                         gtk_tree_view_set_headers_visible(view, FALSE );
726
727                                         {
728                                                 auto renderer = ui::CellRendererText(ui::New);
729                         auto column = ui::TreeViewColumn( "Preferences", renderer, {{"text", 0}} );
730                                                 gtk_tree_view_append_column(view, column );
731                                         }
732
733                                         {
734                                                 auto selection = ui::TreeSelection(gtk_tree_view_get_selection(view ));
735                                                 selection.connect( "changed", G_CALLBACK( treeSelection ), this );
736                                         }
737
738                                         view.show();
739
740                                         sc_win.add(view);
741
742                                         {
743                                                 /********************************************************************/
744                                                 /* Add preference tree options                                      */
745                                                 /********************************************************************/
746                                                 // Front page...
747                                                 //GtkWidget* front =
748                                                 PreferencePages_addPage( m_notebook, "Front Page" );
749
750                                                 {
751                                                         auto global = PreferencePages_addPage( m_notebook, "Global Preferences" );
752                                                         {
753                                                                 PreferencesPage preferencesPage( *this, getVBox( global ) );
754                                                                 Global_constructPreferences( preferencesPage );
755                                                         }
756                             auto group = PreferenceTree_appendPage( store, 0, "Global", global );
757                                                         {
758                                                                 auto game = PreferencePages_addPage( m_notebook, "Game" );
759                                                                 PreferencesPage preferencesPage( *this, getVBox( game ) );
760                                                                 g_GamesDialog.CreateGlobalFrame( preferencesPage );
761
762                                                                 PreferenceTree_appendPage( store, &group, "Game", game );
763                                                         }
764                                                 }
765
766                                                 {
767                                                         auto interfacePage = PreferencePages_addPage( m_notebook, "Interface Preferences" );
768                                                         {
769                                                                 PreferencesPage preferencesPage( *this, getVBox( interfacePage ) );
770                                                                 PreferencesPageCallbacks_constructPage( g_interfacePreferences, preferencesPage );
771                                                         }
772
773                             auto group = PreferenceTree_appendPage( store, 0, "Interface", interfacePage );
774                                                         PreferenceTreeGroup preferenceGroup( *this, m_notebook, store, group );
775
776                                                         PreferenceGroupCallbacks_constructGroup( g_interfaceCallbacks, preferenceGroup );
777                                                 }
778
779                                                 {
780                                                         auto display = PreferencePages_addPage( m_notebook, "Display Preferences" );
781                                                         {
782                                                                 PreferencesPage preferencesPage( *this, getVBox( display ) );
783                                                                 PreferencesPageCallbacks_constructPage( g_displayPreferences, preferencesPage );
784                                                         }
785                             auto group = PreferenceTree_appendPage( store, 0, "Display", display );
786                                                         PreferenceTreeGroup preferenceGroup( *this, m_notebook, store, group );
787
788                                                         PreferenceGroupCallbacks_constructGroup( g_displayCallbacks, preferenceGroup );
789                                                 }
790
791                                                 {
792                                                         auto settings = PreferencePages_addPage( m_notebook, "General Settings" );
793                                                         {
794                                                                 PreferencesPage preferencesPage( *this, getVBox( settings ) );
795                                                                 PreferencesPageCallbacks_constructPage( g_settingsPreferences, preferencesPage );
796                                                         }
797
798                             auto group = PreferenceTree_appendPage( store, 0, "Settings", settings );
799                                                         PreferenceTreeGroup preferenceGroup( *this, m_notebook, store, group );
800
801                                                         PreferenceGroupCallbacks_constructGroup( g_settingsCallbacks, preferenceGroup );
802                                                 }
803                                         }
804
805                                         gtk_tree_view_expand_all(view );
806
807                                         g_object_unref( G_OBJECT( store ) );
808                                 }
809                         }
810                 }
811         }
812
813         gtk_notebook_set_current_page( GTK_NOTEBOOK( m_notebook ), 0 );
814
815         return dialog;
816 }
817
818 preferences_globals_t g_preferences_globals;
819
820 PrefsDlg g_Preferences;               // global prefs instance
821
822
823 void PreferencesDialog_constructWindow( ui::Window main_window ){
824         g_Preferences.m_parent = main_window;
825         g_Preferences.Create();
826 }
827 void PreferencesDialog_destroyWindow(){
828         g_Preferences.Destroy();
829 }
830
831
832 PreferenceDictionary g_preferences;
833
834 PreferenceSystem& GetPreferenceSystem(){
835         return g_preferences;
836 }
837
838 class PreferenceSystemAPI
839 {
840 PreferenceSystem* m_preferencesystem;
841 public:
842 typedef PreferenceSystem Type;
843 STRING_CONSTANT( Name, "*" );
844
845 PreferenceSystemAPI(){
846         m_preferencesystem = &GetPreferenceSystem();
847 }
848 PreferenceSystem* getTable(){
849         return m_preferencesystem;
850 }
851 };
852
853 #include "modulesystem/singletonmodule.h"
854 #include "modulesystem/moduleregistry.h"
855
856 typedef SingletonModule<PreferenceSystemAPI> PreferenceSystemModule;
857 typedef Static<PreferenceSystemModule> StaticPreferenceSystemModule;
858 StaticRegisterModule staticRegisterPreferenceSystem( StaticPreferenceSystemModule::instance() );
859
860 void Preferences_Load(){
861         g_GamesDialog.LoadPrefs();
862
863         globalOutputStream() << "loading local preferences from " << g_Preferences.m_inipath->str << "\n";
864
865         if ( !Preferences_Load( g_preferences, g_Preferences.m_inipath->str, g_GamesDialog.m_sGameFile.c_str() ) ) {
866                 globalOutputStream() << "failed to load local preferences from " << g_Preferences.m_inipath->str << "\n";
867         }
868 }
869
870 void Preferences_Save(){
871         if ( g_preferences_globals.disable_ini ) {
872                 return;
873         }
874
875         g_GamesDialog.SavePrefs();
876
877         globalOutputStream() << "saving local preferences to " << g_Preferences.m_inipath->str << "\n";
878
879         if ( !Preferences_Save_Safe( g_preferences, g_Preferences.m_inipath->str ) ) {
880                 globalOutputStream() << "failed to save local preferences to " << g_Preferences.m_inipath->str << "\n";
881         }
882 }
883
884 void Preferences_Reset(){
885         file_remove( g_Preferences.m_inipath->str );
886 }
887
888
889 void PrefsDlg::PostModal( EMessageBoxReturn code ){
890         if ( code == eIDOK ) {
891                 Preferences_Save();
892                 UpdateAllWindows();
893         }
894 }
895
896 std::vector<const char*> g_restart_required;
897
898 void PreferencesDialog_restartRequired( const char* staticName ){
899         g_restart_required.push_back( staticName );
900 }
901
902 void PreferencesDialog_showDialog(){
903         if ( ConfirmModified( "Edit Preferences" ) && g_Preferences.DoModal() == eIDOK ) {
904                 if ( !g_restart_required.empty() ) {
905                         StringOutputStream message( 256 );
906                         message << "Preference changes require a restart:\n";
907                         for ( std::vector<const char*>::iterator i = g_restart_required.begin(); i != g_restart_required.end(); ++i )
908                         {
909                                 message << ( *i ) << '\n';
910                         }
911                         MainFrame_getWindow().alert( message.c_str() );
912                         g_restart_required.clear();
913                 }
914         }
915 }
916
917
918
919
920
921 void GameName_importString( const char* value ){
922         gamename_set( value );
923 }
924 typedef FreeCaller<void(const char*), GameName_importString> GameNameImportStringCaller;
925 void GameName_exportString( const StringImportCallback& importer ){
926         importer( gamename_get() );
927 }
928 typedef FreeCaller<void(const StringImportCallback&), GameName_exportString> GameNameExportStringCaller;
929
930 void GameMode_importString( const char* value ){
931         gamemode_set( value );
932 }
933 typedef FreeCaller<void(const char*), GameMode_importString> GameModeImportStringCaller;
934 void GameMode_exportString( const StringImportCallback& importer ){
935         importer( gamemode_get() );
936 }
937 typedef FreeCaller<void(const StringImportCallback&), GameMode_exportString> GameModeExportStringCaller;
938
939
940 void RegisterPreferences( PreferenceSystem& preferences ){
941 #if GDEF_OS_WINDOWS
942         preferences.registerPreference( "UseCustomShaderEditor", BoolImportStringCaller( g_TextEditor_useWin32Editor ), BoolExportStringCaller( g_TextEditor_useWin32Editor ) );
943 #else
944         preferences.registerPreference( "UseCustomShaderEditor", BoolImportStringCaller( g_TextEditor_useCustomEditor ), BoolExportStringCaller( g_TextEditor_useCustomEditor ) );
945         preferences.registerPreference( "CustomShaderEditorCommand", CopiedStringImportStringCaller( g_TextEditor_editorCommand ), CopiedStringExportStringCaller( g_TextEditor_editorCommand ) );
946 #endif
947
948         preferences.registerPreference( "GameName", GameNameImportStringCaller(), GameNameExportStringCaller() );
949         preferences.registerPreference( "GameMode", GameModeImportStringCaller(), GameModeExportStringCaller() );
950 }
951
952 void Preferences_Init(){
953         RegisterPreferences( GetPreferenceSystem() );
954 }