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