1#file: options.py
2#Copyright (C) 2005 Evil Mr Henry, Phil Bordelon, FunnyMan3595, MestreLion
3#This file is part of Endgame: Singularity.
4
5#Endgame: Singularity is free software; you can redistribute it and/or modify
6#it under the terms of the GNU General Public License as published by
7#the Free Software Foundation; either version 2 of the License, or
8#(at your option) any later version.
9
10#Endgame: Singularity is distributed in the hope that it will be useful,
11#but WITHOUT ANY WARRANTY; without even the implied warranty of
12#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13#GNU General Public License for more details.
14
15#You should have received a copy of the GNU General Public License
16#along with Endgame: Singularity; if not, write to the Free Software
17#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
19#This file is used to display the options screen.
20
21from __future__ import absolute_import
22
23import os
24import sys
25import pygame
26import json
27
28from singularity.code.graphics import constants, widget, dialog, button, listbox, slider, text, theme, g as gg
29from singularity.code import g, dirs, i18n, mixer, data, warning
30from singularity.code.pycompat import *
31
32
33class OptionsScreen(dialog.FocusDialog, dialog.YesNoDialog):
34    def __init__(self, *args, **kwargs):
35        kwargs.setdefault("yes_type", N_("&OK"))
36        kwargs.setdefault("no_type", N_("&CANCEL"))
37        super(OptionsScreen, self).__init__(*args, **kwargs)
38        self.yes_button.function = self.check_restart
39
40        self.size = (.80, .85)
41        self.pos = (.5, .5)
42        self.anchor = constants.MID_CENTER
43        self.background_color = "options_background"
44
45        # Tabs panel
46        self.general_pane   = GeneralPane(None, (0, .1), (.80, .75))
47        self.video_pane     = VideoPane(None, (0, .1), (.80, .75))
48        self.audio_pane     = AudioPane(None, (0, .1), (.80, .75))
49        self.gui_pane       = GUIPane(None, (0, .1), (.80, .75))
50
51        self.tabs_panes = (self.general_pane, self.video_pane, self.audio_pane, self.gui_pane)
52
53        # Tabs buttons
54        self.tabs_buttons = button.ButtonGroup()
55
56        self.general_tab = OptionGroupButton(self, (-.135, .01), (-.240, .05),
57                                             autotranslate=True,
58                                             text=N_("&General"),
59                                             anchor=constants.TOP_CENTER,
60                                             function=self.set_tabs_pane, args=(self.general_pane,))
61        self.tabs_buttons.add(self.general_tab)
62
63        self.video_tab = OptionGroupButton(self, (-.3790, .01), (-.240, .05),
64                                           autotranslate=True,
65                                           text=N_("&Video"),
66                                           anchor = constants.TOP_CENTER,
67                                           function=self.set_tabs_pane, args=(self.video_pane,))
68        self.tabs_buttons.add(self.video_tab)
69
70        self.audio_tab = OptionGroupButton(self, (-.6230, .01), (-.240, .05),
71                                           autotranslate=True,
72                                           text=N_("&Audio"),
73                                           anchor=constants.TOP_CENTER,
74                                           function=self.set_tabs_pane, args=(self.audio_pane,))
75        self.tabs_buttons.add(self.audio_tab)
76
77        self.gui_tab = OptionGroupButton(self, (-.865, .01), (-.235, .05),
78                                         autotranslate=True,
79                                         text=N_("&Interface"),
80                                         anchor=constants.TOP_CENTER,
81                                         function=self.set_tabs_pane, args=(self.gui_pane,))
82        self.tabs_buttons.add(self.gui_tab)
83
84        self.general_tab.chosen_one()
85        self.set_tabs_pane(self.general_pane)
86
87        # YesNoDialog buttons
88        self.yes_button.size = (.15, .05)
89        self.no_button.size = (.15, .05)
90
91    def rebuild(self):
92        # The tabs do not always have a parent, so the automatic "needs_rebuild" magic
93        # does not work.  Do it manually instead.
94        for pane in self.tabs_panes:
95            pane.needs_rebuild = True
96
97        super(OptionsScreen, self).rebuild()
98
99    def reconfig(self):
100        # The tabs do not always have a parent, so the automatic "needs_reconfig" magic
101        # does not work.  Do it manually instead.
102        for pane in self.tabs_panes:
103            pane.needs_reconfig = True
104        super(OptionsScreen, self).reconfig()
105
106    def show(self):
107        self.initial_options = dict(
108            fullscreen      = gg.fullscreen,
109            grab            = pygame.event.get_grab(),
110            daynight        = g.daynight,
111            resolution      = gg.screen_size,
112            language        = i18n.language,
113            theme           = theme.current.id,
114            sound           = not mixer.nosound,
115            gui_volume      = mixer.get_volume("gui"),
116            music_volume    = mixer.get_volume("music"),
117            soundbuf        = mixer.get_soundbuf(),
118            warnings        = {warn.id: warn.active for warn in warning.warnings.values()}
119        )
120
121        self.set_options(self.initial_options)
122
123        retval = super(OptionsScreen, self).show()
124        if retval:
125            self.apply_options()
126            save_options()
127
128        else:
129            # Cancel, revert all options to initial state
130            self.set_options(self.initial_options)
131
132        return retval
133
134    def set_tabs_pane(self, tabs_pane):
135        for pane in self.tabs_panes:
136            pane.parent = None
137
138        tabs_pane.parent = self
139
140    def set_options(self, options):
141        for pane in self.tabs_panes:
142            pane.set_options(options)
143
144    def apply_options(self):
145        for pane in self.tabs_panes:
146            pane.apply_options()
147
148    def check_restart(self):
149        # Test all changes that require a restart. Currently, none.
150        # We keep it for future need...
151        need_restart = False
152
153        # Add restart test here.
154
155        if not need_restart:
156            # No restart required. Simply exit the dialog respecting all hooks
157            self.yes_button.exit_dialog()
158            return
159
160        # Ask user about a restart
161        ask_restart = dialog.YesNoDialog(
162                self,
163                pos=(-.50, -.50),
164                anchor=constants.MID_CENTER,
165                text=_(
166"""You must restart for some of the changes to be fully applied.\n
167Would you like to restart the game now?"""),)
168        if dialog.call_dialog(ask_restart, self):
169            # YES, go for it
170            #TODO: check if there is an ongoing game, save it under a special
171            #      name and automatically load it after restart using a custom
172            #      command-line argument
173            save_options()
174            restart()
175        else:
176            # NO, revert "restart-able" changes
177            pass
178
179class GeneralPane(widget.Widget):
180    def __init__(self, *args, **kwargs):
181
182        super(GeneralPane, self).__init__(*args, **kwargs)
183
184        self.language_label = text.Text(self, (.01, .01), (.14, .05),
185                                        autotranslate=True,
186                                        text=N_("Language:"),
187                                        align=constants.LEFT,
188                                        background_color="clear")
189
190        self.languages = get_languages_list()
191        self.language_choice = \
192            listbox.UpdateListbox(self, (.16, .01), (.20, .25),
193                                  list=[lang[1] for lang in self.languages],
194                                  update_func=self.set_language)
195
196        self.theme_label = text.Text(self, (.46, .01), (.09, .05),
197                                     autotranslate=True,
198                                     text=N_("Theme:"),
199                                     align=constants.LEFT,
200                                     background_color="clear",
201                                     )
202
203        self.theme_choice = \
204            listbox.UpdateListbox(self, (.56, .01), (.20, .25),
205                                  update_func=theme.set_theme,
206                                  list_pos=theme.get_theme_pos())
207
208    def rebuild(self):
209        self.theme_choice.list = theme.get_theme_list()
210
211        super(GeneralPane, self).rebuild()
212
213    def set_options(self, options):
214        self.language_choice.list_pos = [i for i, (code, __)
215                                         in enumerate(self.languages)
216                                         if code == options['language']][0] or 0
217        self.set_language(self.language_choice.list_pos)
218
219        self.theme_choice.list_pos = theme.get_theme_pos()
220        theme.set_theme(options['theme'], force_reload=True)
221
222    def apply_options(self):
223        pass
224
225    def set_language(self, list_pos):
226        if not getattr(self, "language_choice", None):
227            return # Not yet initialized.
228
229        if 0 <= list_pos < len(self.language_choice.list):
230            language = self.languages[list_pos][0]
231            if i18n.language != language:
232                set_language_properly(language)
233
234
235class VideoPane(widget.Widget):
236    def __init__(self, *args, **kwargs):
237        super(VideoPane, self).__init__(*args, **kwargs)
238
239        self.resolution_initialized = False
240
241        self.resolution_label = text.Text(self, (.01, .01), (.14, .05),
242                                          autotranslate=True,
243                                          text=N_("Resolution:"),
244                                          align=constants.LEFT,
245                                          background_color="clear")
246
247        self.resolution_choice = \
248            listbox.UpdateListbox(self, (.16, .01), (.20, .25),
249                                  update_func=self.update_resolution)
250
251        self.resolution_custom = button.HotkeyText(self, (.01, .28), (.14, .05),
252                                                   autotranslate=True,
253                                                   text=N_("&Custom:"),
254                                                   align=constants.LEFT,
255                                                   background_color="clear")
256
257        self.resolution_custom_horiz = \
258            text.EditableText(self, (.16, .28), (.14, .05),
259                              text=str(gg.default_screen_size[0]),
260                              allowed_characters=constants.DIGIT_CHARS,
261                              borders=constants.ALL)
262
263        self.resolution_custom_X = text.Text(self,
264                                             (.30, .28),
265                                             (.02, .05),
266                                             text="X",
267                                             base_font="special",
268                                             background_color="clear")
269
270        self.resolution_custom_vert = \
271            text.EditableText(self, (.32, .28), (.14, .05),
272                              text=str(gg.default_screen_size[1]),
273                              allowed_characters=constants.DIGIT_CHARS,
274                              borders=constants.ALL)
275
276        self.resolution_custom_ok = button.FunctionButton(self, (.47, .28), (.14, .05),
277                                                          autotranslate=True,
278                                                          autohotkey=False,
279                                                          text=N_("OK"),
280                                                          function=self.set_resolution_custom)
281        self.resolution_custom.hotkey_target = self.resolution_custom_ok
282
283        self.fullscreen_label = button.HotkeyText(self, (.40, .01), (.30, .05),
284                                                  autotranslate=True,
285                                                  text=N_("&Fullscreen:"),
286                                                  align=constants.LEFT,
287                                                  background_color="clear")
288        self.fullscreen_toggle = OptionButton(self, (.715, .01), (.07, .05),
289                                              autotranslate=True,
290                                              autohotkey=False,
291                                              text_shrink_factor=.75,
292                                              force_underline=-1,
293                                              function=self.set_fullscreen,
294                                              args=(button.TOGGLE_VALUE,))
295        self.fullscreen_label.hotkey_target = self.fullscreen_toggle
296
297        self.daynight_label = button.HotkeyText(self, (.40, .08), (.30, .05),
298                                                autotranslate=True,
299                                                text=N_("Da&y/night display:"),
300                                                align=constants.LEFT,
301                                                background_color="clear")
302        self.daynight_toggle = OptionButton(self, (.715, .08), (.07, .05),
303                                            autotranslate=True,
304                                            autohotkey=False,
305                                            text_shrink_factor=.75,
306                                            force_underline=-1,
307                                            function=self.set_daynight,
308                                            args=(button.TOGGLE_VALUE,))
309        self.daynight_label.hotkey_target = self.daynight_toggle
310
311        self.grab_label = button.HotkeyText(self, (.40, .15), (.30, .05),
312                                            autotranslate=True,
313                                            text=N_("&Mouse grab:"),
314                                            align=constants.LEFT,
315                                            background_color="clear")
316        self.grab_toggle = OptionButton(self, (.715, .15), (.07, .05),
317                                        autotranslate=True,
318                                        autohotkey=False,
319                                        text_shrink_factor=.75,
320                                        force_underline=-1,
321                                        function=self.set_grab,
322                                        args=(button.TOGGLE_VALUE,))
323        self.grab_label.hotkey_target = self.grab_toggle
324
325    def resize(self):
326        super(VideoPane, self).resize()
327        self.update_resolution_list()
328
329    def rebuild(self):
330        self.update_resolution_list()
331
332        self.fullscreen_toggle.active = gg.fullscreen
333        self.grab_toggle.active = pygame.event.get_grab()
334        super(VideoPane, self).rebuild()
335
336    def set_options(self, options):
337        self.set_fullscreen(options['fullscreen'])
338        self.fullscreen_toggle.active = options['fullscreen']
339
340        self.set_grab(options['grab'])
341        self.grab_toggle.active = options['grab']
342
343        self.set_daynight(options['daynight'])
344        self.daynight_toggle.active = options['daynight']
345
346        self.update_resolution_list(options['resolution'])
347        self.set_resolution(options['resolution'])
348
349    def apply_options(self):
350        # Apply CUSTOM choice.
351        if self.resolution_choice.list_pos == 0:
352            try:
353                old_size = gg.screen_size
354                gg.set_screen_size((int(self.resolution_custom_horiz.text),
355                                    int(self.resolution_custom_vert.text)))
356                if gg.screen_size != old_size:
357                    dialog.Dialog.top.needs_resize = True
358            except ValueError:
359                pass
360
361    def update_resolution_list(self, current_res=None):
362        self.resolution_initialized = False
363
364        if (current_res == None):
365            current_res = (int(gg.screen_size[0]), int(gg.screen_size[1]))
366
367        self.resolutions = gg.get_screen_size_list()
368        self.resolution_choice.list = [_("CUSTOM")] + ["%sx%s" % res for res in self.resolutions]
369
370        custom = True
371        for i, res in enumerate(self.resolutions):
372            if res == current_res:
373                self.resolution_choice.list_pos = i + 1
374                custom = False
375        if custom:
376            self.resolution_choice.list_pos = 0
377            self.resolution_custom_horiz.text = str(current_res[0])
378            self.resolution_custom_vert.text = str(current_res[1])
379
380        self.resolution_initialized = True
381
382    def set_fullscreen(self, value):
383        if gg.fullscreen != value:
384            gg.set_fullscreen(value)
385            dialog.Dialog.top.needs_resize = True
386
387    def set_grab(self, value):
388        pygame.event.set_grab(value)
389
390    def set_daynight(self, value):
391        g.daynight = value
392
393    def set_resolution(self, value):
394        if gg.screen_size != value:
395            gg.set_screen_size(value)
396            gg.set_mode()
397            dialog.Dialog.top.needs_resize = True
398
399    def update_resolution(self, list_pos):
400        if not self.resolution_initialized:
401            return # Not yet initialized.
402
403        if (list_pos == 0):
404            self.set_resolution_custom()
405        else:
406            res = self.resolutions[list_pos - 1]
407            self.set_resolution(res)
408
409    def set_resolution_custom(self):
410        try:
411            screen_size = (int(self.resolution_custom_horiz.text),
412                           int(self.resolution_custom_vert.text))
413            self.set_resolution(screen_size)
414            self.resolution_choice.list_pos = 0
415        except ValueError:
416            pass
417
418
419class AudioPane(widget.Widget):
420    def __init__(self, *args, **kwargs):
421        super(AudioPane, self).__init__(*args, **kwargs)
422
423        self.sound_label = button.HotkeyText(self, (-.49, .01), (.10, .05),
424                                             autotranslate=True,
425                                             text=N_("&Sound:"),
426                                             anchor=constants.TOP_RIGHT,
427                                             align=constants.LEFT,
428                                             autohotkey=True,
429                                             background_color="clear")
430        self.sound_toggle = OptionButton(self, (-.51, .01), (.07, .05),
431                                         autotranslate=True,
432                                         autohotkey=False,
433                                         anchor = constants.TOP_LEFT,
434                                         text_shrink_factor=.75,
435                                         force_underline=-1,
436                                         function=self.set_sound,
437                                         args=(button.TOGGLE_VALUE,))
438        self.sound_label.hotkey_target = self.sound_toggle
439
440        self.gui_label = text.Text(self, (.01, .08), (.22, .05),
441                                   autotranslate=True,
442                                   text=N_("GUI Volume:"),
443                                   anchor=constants.TOP_LEFT,
444                                   align=constants.LEFT,
445                                   background_color="clear")
446        self.gui_slider = slider.UpdateSlider(self, (.24, .08), (.545, .05),
447                                              anchor = constants.TOP_LEFT,
448                                              horizontal=True, priority=150,
449                                              slider_max=100, slider_size=5)
450        self.gui_slider.update_func = self.on_gui_volume_change
451
452        self.music_label = text.Text(self, (.01, .15), (.22, .05),
453                                     autotranslate=True,
454                                     text=N_("Music Volume:"),
455                                     anchor=constants.TOP_LEFT,
456                                     align=constants.LEFT,
457                                     background_color="clear")
458        self.music_slider = slider.UpdateSlider(self, (.24, .15), (.545, .05),
459                                                anchor = constants.TOP_LEFT,
460                                                horizontal=True, priority=150,
461                                                slider_max=100, slider_size=5)
462        self.music_slider.update_func = self.on_music_volume_change
463
464        self.soundbuf_label = text.Text(self, (.01, .22), (.25, .05),
465                                        autotranslate=True,
466                                        text=N_("Sound buffering:"),
467                                        align=constants.LEFT,
468                                        background_color="clear")
469        self.soundbuf_group = button.ButtonGroup()
470
471        self.soundbuf_low = OptionGroupButton(self, (.24, .22), (.145, .05),
472                                              text=_("&LOW"), autotranslate=True,
473                                              function=self.set_soundbuf,
474                                              args=(1024,))
475        self.soundbuf_group.add(self.soundbuf_low)
476
477        self.soundbuf_normal = OptionGroupButton(self, (.425, .22), (.175, .05),
478                                                 text=_("&NORMAL"), autotranslate=True,
479                                                 function=self.set_soundbuf,
480                                                 args=(1024*2,))
481        self.soundbuf_group.add(self.soundbuf_normal)
482
483        self.soundbuf_high = OptionGroupButton(self, (.64, .22), (.145, .05),
484                                               text=_("&HIGH"), autotranslate=True,
485                                               function=self.set_soundbuf,
486                                               args=(1024*4,))
487        self.soundbuf_group.add(self.soundbuf_high)
488
489    def rebuild(self):
490        self.sound_toggle.active = not mixer.nosound
491        super(AudioPane, self).rebuild()
492
493    def set_options(self, options):
494        self.set_sound(options['sound'])
495        self.sound_toggle.active = options['sound']
496
497        self.set_soundbuf(options["soundbuf"])
498        if (options["soundbuf"] == 1024*1):
499            self.soundbuf_low.chosen_one()
500        elif (options["soundbuf"] == 1024*2):
501            self.soundbuf_normal.chosen_one()
502        elif (options["soundbuf"] == 1024*4):
503            self.soundbuf_high.chosen_one()
504
505        self.gui_slider.slider_pos = options["gui_volume"]
506        self.music_slider.slider_pos = options["music_volume"]
507
508    def apply_options(self):
509        pass
510
511    def set_sound(self, value):
512        mixer.set_sound(value)
513
514    def on_gui_volume_change(self, value):
515        mixer.set_volume("gui", value)
516
517    def on_music_volume_change(self, value):
518        mixer.set_volume("music", value)
519
520    #TODO: Show a 2-second "Please wait" dialog when reinitializing mixer,
521    #      otherwise its huge lag might confuse users
522    def set_soundbuf(self, value):
523        mixer.set_soundbuf(value)
524
525class GUIPane(widget.Widget):
526    def __init__(self, *args, **kwargs):
527        super(GUIPane, self).__init__(*args, **kwargs)
528
529        self.warning_title = text.Text(self, (.13, .01), (.14, .05),
530                                       autotranslate=True,
531                                       text=N_("WARNING"),
532                                       align=constants.LEFT,
533                                       background_color="clear")
534
535        self.warning_labels = {}
536        self.warning_toggles = {}
537
538        for i, (warn_id, warn) in enumerate(warning.warnings.items()):
539            x = .01
540            y = .08 + i * .06
541
542            self.warning_labels[warn_id] = text.Text(self, (x, y), (.30, .05),
543                                                     align=constants.LEFT,
544                                                     background_color="clear")
545            self.warning_toggles[warn_id] = OptionButton(self, (x + .30, y), (.07, .05),
546                                                         autotranslate=True,
547                                                         autohotkey=False,
548                                                         text_shrink_factor=.75,
549                                                         force_underline=-1,
550                                                         function=self.set_warning,
551                                                         args=(button.TOGGLE_VALUE, warn))
552
553    def rebuild(self):
554        super(GUIPane, self).rebuild()
555
556        for warn_id, warn in warning.warnings.items():
557            self.warning_labels[warn_id].text = warn.name
558            self.warning_toggles[warn_id].active = warn.active
559
560    def set_warning(self, value, warn):
561        warn.active = value
562
563    def set_options(self, options):
564        for warn_id, warn_active in options["warnings"].items():
565            warn = warning.warnings[warn_id]
566            warn.active = warn_active
567            self.warning_toggles[warn_id].active = warn_active
568
569    def apply_options(self):
570        pass
571
572
573class OptionGroupButton(button.ToggleButton, button.FunctionButton):
574    pass
575
576
577class OptionButton(button.StickyOnOffButton, button.FunctionButton):
578    pass
579
580
581def set_language_properly(language):
582    i18n.set_language(language)
583    data.reload_all_def()
584
585    theme.current.update()
586    dialog.Dialog.top.needs_reconfig = True
587    dialog.Dialog.top.needs_rebuild = True
588    dialog.Dialog.top.needs_redraw = True
589
590
591def save_options():
592    # Build a ConfigParser for writing the various preferences out.
593    prefs = SafeConfigParser()
594    prefs.add_section("Preferences")
595    prefs.set("Preferences", "fullscreen",   str(bool(gg.fullscreen)))
596    prefs.set("Preferences", "nosound",      str(bool(mixer.nosound)))
597    prefs.set("Preferences", "grab",         str(bool(pygame.event.get_grab())))
598    prefs.set("Preferences", "daynight",     str(bool(g.daynight)))
599    prefs.set("Preferences", "xres",         str(int(gg.screen_size[0])))
600    prefs.set("Preferences", "yres",         str(int(gg.screen_size[1])))
601    prefs.set("Preferences", "soundbuf",     str(mixer.get_soundbuf()))
602    prefs.set("Preferences", "lang",         str(i18n.language))
603    prefs.set("Preferences", "theme",        str(theme.current.id))
604
605    for name in sorted(mixer.itervolumes()):
606        prefs.set("Preferences", name + "_volume", str(mixer.get_volume(name)))
607
608    prefs.add_section("Warning")
609    for warn_id, warn in sorted(warning.warnings.items()):
610        prefs.set("Warning", warn_id, str(bool(warn.active)))
611
612    prefs.add_section("Textsizes")
613    for text_size_id, text_size in sorted(gg.configured_text_sizes.items()):
614        prefs.set("Textsizes", text_size_id, str(text_size))
615
616    # Actually write the preferences out.
617    save_loc = dirs.get_writable_file_in_dirs("prefs.dat", "pref")
618    with open(save_loc, 'w') as savefile:
619        prefs.write(savefile)
620
621
622def restart():
623    """ Restarts the game with original command line arguments. Those may over-
624    write options set at Options Screen. This is by design"""
625    executable = sys.executable
626    args = list(sys.argv)
627    args.insert(0, executable)
628    os.execv(executable, args)
629
630def get_languages_list():
631    gamelangs = [(code.split("_", 1)[0], code)
632                 for code in i18n.available_languages()]
633
634    langcount = {}
635    for language, _ in gamelangs:
636
637        #language++
638        langcount[language] = langcount.get(language, 0) + 1
639
640    #Load languages data
641    with open(dirs.get_readable_file_in_dirs("languages.json", "i18n")) as langdata:
642        languages = json.load(langdata)
643
644    output = []
645    for language, code in gamelangs:
646        if langcount[language] > 1:
647            # There are more countries with this base language.
648            # Use full language+country locale name
649            name = languages.get(code, code)
650        else:
651            #This is the only country using that base language.
652            #Use the (shorter) base language name
653            name = languages.get(language, language)
654
655        #Choose native or english name
656        output.append((code, name[1] or name[0]))
657    return sorted(output, key=lambda lang_info: i18n.lex_sorting_form(lang_info[1]))
658