ok
[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
30 #include "debugging/debugging.h"
31
32 #include <gtk/gtkmain.h>
33 #include <gtk/gtkvbox.h>
34 #include <gtk/gtkhbox.h>
35 #include <gtk/gtkframe.h>
36 #include <gtk/gtklabel.h>
37 #include <gtk/gtktogglebutton.h>
38 #include <gtk/gtkspinbutton.h>
39 #include <gtk/gtkscrolledwindow.h>
40 #include <gtk/gtktreemodel.h>
41 #include <gtk/gtktreeview.h>
42 #include <gtk/gtktreestore.h>
43 #include <gtk/gtktreeselection.h>
44 #include <gtk/gtkcellrenderertext.h>
45 #include <gtk/gtknotebook.h>
46
47 #include "generic/callback.h"
48 #include "math/vector.h"
49 #include "string/string.h"
50 #include "stream/stringstream.h"
51 #include "os/file.h"
52 #include "os/path.h"
53 #include "os/dir.h"
54 #include "gtkutil/filechooser.h"
55 #include "gtkutil/messagebox.h"
56 #include "cmdlib.h"
57
58 #include "error.h"
59 #include "console.h"
60 #include "xywindow.h"
61 #include "mainframe.h"
62 #include "qe3.h"
63 #include "gtkdlgs.h"
64
65
66
67 void Global_constructPreferences(PreferencesPage& page)
68 {
69   page.appendCheckBox("Console", "Enable Logging", g_Console_enableLogging);
70 }
71
72 void Interface_constructPreferences(PreferencesPage& page)
73 {
74 #ifdef WIN32
75   page.appendCheckBox("", "Native File-Chooser", g_FileChooser_nativeGUI);
76   page.appendCheckBox("", "Default Text Editor", g_TextEditor_useWin32Editor);
77 #else
78   {
79     GtkWidget* use_custom = page.appendCheckBox("Text Editor", "Custom", g_TextEditor_useCustomEditor);
80     GtkWidget* custom_editor = page.appendPathEntry("Text Editor Command", g_TextEditor_editorCommand, true);
81     Widget_connectToggleDependency(custom_editor, use_custom);
82   }
83 #endif
84 }
85
86 void Mouse_constructPreferences(PreferencesPage& page)
87 {
88   {
89     const char* buttons[] = { "2 button", "3 button", };
90     page.appendRadio("Mouse Type",  g_glwindow_globals.m_nMouseType, STRING_ARRAY_RANGE(buttons));
91   }
92   page.appendCheckBox("Right Button", "Activates Context Menu", g_xywindow_globals.m_bRightClick);
93 }
94 void Mouse_constructPage(PreferenceGroup& group)
95 {
96   PreferencesPage page(group.createPage("Mouse", "Mouse Preferences"));
97   Mouse_constructPreferences(page);
98 }
99 void Mouse_registerPreferencesPage()
100 {
101   PreferencesDialog_addInterfacePage(FreeCaller1<PreferenceGroup&, Mouse_constructPage>());
102 }
103
104
105 /*!
106 =========================================================
107 Games selection dialog
108 =========================================================
109 */
110
111 #include <map>
112
113 inline const char* xmlAttr_getName(xmlAttrPtr attr)
114 {
115   return reinterpret_cast<const char*>(attr->name);
116 }
117
118 inline const char* xmlAttr_getValue(xmlAttrPtr attr)
119 {
120   return reinterpret_cast<const char*>(attr->children->content);
121 }
122
123 CGameDescription::CGameDescription(xmlDocPtr pDoc, const CopiedString& gameFile)
124 {
125   // read the user-friendly game name 
126   xmlNodePtr pNode = pDoc->children;
127
128   while (strcmp((const char*)pNode->name, "game") && pNode != 0)
129   {
130     pNode=pNode->next;
131   }
132   if (!pNode)
133   {
134     Error("Didn't find 'game' node in the game description file '%s'\n", pDoc->URL);
135   }
136
137   for(xmlAttrPtr attr = pNode->properties; attr != 0; attr = attr->next)
138   {
139     m_gameDescription.insert(GameDescription::value_type(xmlAttr_getName(attr), xmlAttr_getValue(attr)));
140   }
141
142   {
143     StringOutputStream path(256);
144     path << AppPath_get() << gameFile.c_str() << "/";
145     mGameToolsPath = path.c_str();
146   }
147
148   ASSERT_MESSAGE(file_exists(mGameToolsPath.c_str()), "game directory not found: " << makeQuoted(mGameToolsPath.c_str()));
149
150   mGameFile = gameFile;
151  
152   {
153     GameDescription::iterator i = m_gameDescription.find("type");
154     if(i == m_gameDescription.end())
155     {
156       globalErrorStream() << "Warning, 'type' attribute not found in '" << reinterpret_cast<const char*>(pDoc->URL) << "'\n";
157       // default
158       mGameType = "q3";
159     }
160     else
161     {
162       mGameType = (*i).second.c_str();
163     }
164   }
165 }
166
167 void CGameDescription::Dump()
168 {
169   globalOutputStream() << "game description file: " << makeQuoted(mGameFile.c_str()) << "\n";
170   for(GameDescription::iterator i = m_gameDescription.begin(); i != m_gameDescription.end(); ++i)
171   {
172     globalOutputStream() << (*i).first.c_str() << " = " << makeQuoted((*i).second.c_str()) << "\n";
173   }
174 }
175
176 CGameDescription *g_pGameDescription; ///< shortcut to g_GamesDialog.m_pCurrentDescription
177
178
179 #include "warnings.h"
180 #include "stream/textfilestream.h"
181 #include "container/array.h"
182 #include "xml/ixml.h"
183 #include "xml/xmlparser.h"
184 #include "xml/xmlwriter.h"
185
186 #include "preferencedictionary.h"
187 #include "stringio.h"
188
189 const char* const PREFERENCES_VERSION = "1.0";
190
191 bool Preferences_Load(PreferenceDictionary& preferences, const char* filename)
192 {
193   TextFileInputStream file(filename);
194   if(!file.failed())
195   {
196     XMLStreamParser parser(file);
197     XMLPreferenceDictionaryImporter importer(preferences, PREFERENCES_VERSION);
198     parser.exportXML(importer);
199     return true;
200   }
201   return false;
202 }
203
204 bool Preferences_Save(PreferenceDictionary& preferences, const char* filename)
205 {
206   TextFileOutputStream file(filename);
207   if(!file.failed())
208   {
209     XMLStreamWriter writer(file);
210     XMLPreferenceDictionaryExporter exporter(preferences, PREFERENCES_VERSION);
211     exporter.exportXML(writer);
212     return true;
213   }
214   return false;
215 }
216
217 bool Preferences_Save_Safe(PreferenceDictionary& preferences, const char* filename)
218 {
219   Array<char> tmpName(filename, filename + strlen(filename) + 1 + 3);
220   *(tmpName.end() - 4) = 'T';
221   *(tmpName.end() - 3) = 'M';
222   *(tmpName.end() - 2) = 'P';
223   *(tmpName.end() - 1) = '\0';
224
225   return Preferences_Save(preferences, tmpName.data())
226     && (!file_exists(filename) || file_remove(filename))
227     && file_move(tmpName.data(), filename);
228 }
229
230
231
232 void LogConsole_importString(const char* string)
233 {
234   g_Console_enableLogging = string_equal(string, "true");
235   Sys_LogFile(g_Console_enableLogging);
236 }
237 typedef FreeCaller1<const char*, LogConsole_importString> LogConsoleImportStringCaller;
238
239
240 void RegisterGlobalPreferences(PreferenceSystem& preferences)
241 {
242   preferences.registerPreference("gamefile", CopiedStringImportStringCaller(g_GamesDialog.m_sGameFile), CopiedStringExportStringCaller(g_GamesDialog.m_sGameFile));
243   preferences.registerPreference("gamePrompt", BoolImportStringCaller(g_GamesDialog.m_bGamePrompt), BoolExportStringCaller(g_GamesDialog.m_bGamePrompt));
244   preferences.registerPreference("log console", LogConsoleImportStringCaller(), BoolExportStringCaller(g_Console_enableLogging));
245 }
246
247
248 PreferenceDictionary g_global_preferences;
249
250 void GlobalPreferences_Init()
251 {
252   RegisterGlobalPreferences(g_global_preferences);
253 }
254
255 void CGameDialog::LoadPrefs()
256 {
257   // load global .pref file
258   StringOutputStream strGlobalPref(256);
259   strGlobalPref << g_Preferences.m_global_rc_path->str << "global.pref";
260
261   globalOutputStream() << "loading global preferences from " << makeQuoted(strGlobalPref.c_str()) << "\n";
262
263   if(!Preferences_Load(g_global_preferences, strGlobalPref.c_str()))
264   {
265     globalOutputStream() << "failed to load global preferences from " << strGlobalPref.c_str() << "\n";
266   }
267 }
268
269 void CGameDialog::SavePrefs()
270 {
271   StringOutputStream strGlobalPref(256);
272   strGlobalPref << g_Preferences.m_global_rc_path->str << "global.pref";
273
274   globalOutputStream() << "saving global preferences to " << strGlobalPref.c_str() << "\n";
275
276   if(!Preferences_Save_Safe(g_global_preferences, strGlobalPref.c_str()))
277   {
278     globalOutputStream() << "failed to save global preferences to " << strGlobalPref.c_str() << "\n";
279   }
280 }
281
282 void CGameDialog::DoGameDialog()
283 {
284   // show the UI
285   DoModal();
286
287   // we save the prefs file
288   SavePrefs();
289 }
290
291 void CGameDialog::GameFileImport(int value)
292 {
293   m_nComboSelect = value;
294   // use value to set m_sGameFile
295   std::list<CGameDescription *>::iterator iGame = mGames.begin();
296   int i;
297   for(i=0; i<value; i++)
298   {
299     ++iGame;
300   }
301   m_sGameFile = (*iGame)->mGameFile;
302 }
303
304 void CGameDialog::GameFileExport(const IntImportCallback& importCallback) const
305 {
306   // use m_sGameFile to set value
307   std::list<CGameDescription *>::const_iterator iGame;
308   int i = 0;
309   for(iGame=mGames.begin(); iGame!=mGames.end(); ++iGame)
310   {
311     if ((*iGame)->mGameFile == m_sGameFile)
312     {
313       m_nComboSelect = i;
314       break;
315     }
316     i++;
317   }
318   importCallback(m_nComboSelect);
319 }
320
321 void CGameDialog_GameFileImport(CGameDialog& self, int value)
322 {
323   self.GameFileImport(value);
324 }
325
326 void CGameDialog_GameFileExport(CGameDialog& self, const IntImportCallback& importCallback)
327 {
328   self.GameFileExport(importCallback);
329 }
330
331 void CGameDialog::CreateGlobalFrame(PreferencesPage& page)
332 {
333   std::vector<const char*> games;
334   games.reserve(mGames.size());
335   for(std::list<CGameDescription *>::iterator i = mGames.begin(); i != mGames.end(); ++i)
336   {
337     games.push_back((*i)->getRequiredKeyValue("name"));
338   }
339   page.appendCombo(
340     "Select the game",
341     StringArrayRange(&(*games.begin()), &(*games.end())),
342     ReferenceCaller1<CGameDialog, int, CGameDialog_GameFileImport>(*this),
343     ReferenceCaller1<CGameDialog, const IntImportCallback&, CGameDialog_GameFileExport>(*this)
344   );
345   page.appendCheckBox("Startup", "Show Global Preferences", m_bGamePrompt);
346 }
347
348 GtkWindow* CGameDialog::BuildDialog()
349 {
350   GtkFrame* frame = create_dialog_frame("Game settings", GTK_SHADOW_ETCHED_IN);
351
352   GtkVBox* vbox2 = create_dialog_vbox(0, 4);
353   gtk_container_add(GTK_CONTAINER(frame), GTK_WIDGET(vbox2));
354
355   {
356     PreferencesPage preferencesPage(*this, GTK_WIDGET(vbox2));
357     Global_constructPreferences(preferencesPage);
358     CreateGlobalFrame(preferencesPage);
359   }
360
361   return create_simple_modal_dialog_window("Global Preferences", m_modal, GTK_WIDGET(frame));
362 }
363
364 class LoadGameFile
365 {
366   std::list<CGameDescription*>& mGames;
367   const char* mPath;
368 public:
369   LoadGameFile(std::list<CGameDescription*>& games, const char* path) : mGames(games), mPath(path)
370   {
371   }
372   void operator()(const char* name) const
373   {
374     if(!extension_equal(path_get_extension(name), "game"))
375     {
376       return;
377     }
378     StringOutputStream strPath(256);
379     strPath << mPath << name;
380     globalOutputStream() << strPath.c_str() << '\n';
381
382     xmlDocPtr pDoc = xmlParseFile(strPath.c_str());
383     if(pDoc)
384     {
385       mGames.push_front(new CGameDescription(pDoc, name));
386       xmlFreeDoc(pDoc);
387     }
388     else
389     {
390       globalErrorStream() << "XML parser failed on '" << strPath.c_str() << "'\n";
391     }
392   }
393 };
394
395 void CGameDialog::ScanForGames()
396 {
397   StringOutputStream strGamesPath(256);
398   strGamesPath << AppPath_get() << "games/";
399   const char *path = strGamesPath.c_str();
400
401   globalOutputStream() << "Scanning for game description files: " << path << '\n';
402
403   /*!
404   \todo FIXME LINUX:
405   do we put game description files below AppPath, or in ~/.radiant
406   i.e. read only or read/write?
407   my guess .. readonly cause it's an install
408   we will probably want to add ~/.radiant/<version>/games/ scanning on top of that for developers
409   (if that's really needed)
410   */
411
412   Directory_forEach(path, LoadGameFile(mGames, path));
413 }
414
415 CGameDescription* CGameDialog::GameDescriptionForComboItem()
416 {
417   std::list<CGameDescription *>::iterator iGame;
418   int i=0;
419   for(iGame=mGames.begin(); iGame!=mGames.end(); ++iGame,i++)
420   {
421     if (i == m_nComboSelect)
422     {
423       return (*iGame);
424     }
425   }
426   return 0; // not found
427 }
428
429 void CGameDialog::InitGlobalPrefPath()
430 {
431   g_Preferences.m_global_rc_path = g_string_new(SettingsPath_get());
432 }
433
434 void CGameDialog::Reset()
435 {
436   if (!g_Preferences.m_global_rc_path)