# HG changeset patch # User Emmanuel Gil Peyrot # Date 1404249495 -7200 # Node ID e7a4731a278bbfd55f44270ad617db740524fa4a # Parent b2269b9c6119b59369387ed370eeb852c32ecee5 Add a GTK+ main menu, mimicking the original EoSD one. diff --git a/data/menu.glade b/data/menu.glade new file mode 100644 --- /dev/null +++ b/data/menu.glade @@ -0,0 +1,754 @@ + + + + + + False + PyTouhou + False + False + + + + + True + False + vertical + + + True + False + vertical + + + True + False + Difficulty + + + False + True + 0 + + + + + True + False + 0 + + Easy + Normal + Hard + Lunatic + + + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + vertical + + + True + False + Character + + + False + True + 0 + + + + + True + False + 0 + + Reimu A + Reimu B + Marisa A + Marisa B + + + + + False + True + 1 + + + + + False + True + 1 + + + + + True + False + vertical + + + True + False + Stage + + + False + True + 0 + + + + + True + False + 0 + + Stage 1 + Stage 2 + Stage 3 + Stage 4 + Stage 5 + Final Stage + Extra Stage + + + + + False + True + 1 + + + + + False + True + 2 + + + + + _Boss Rush + True + True + False + True + 0 + True + + + + False + True + 3 + + + + + True + False + start + + + Back + True + True + True + + + + True + True + 0 + + + + + Play + True + True + True + + + + False + True + 1 + + + + + False + True + 4 + + + + + + + False + PyTouhou + False + False + + + + + True + False + vertical + + + True + False + vertical + + + True + False + Game Directory + + + False + True + 0 + + + + + True + False + select-folder + Game Directory + + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + vertical + + + True + False + Backend + + + False + True + 0 + + + + + True + False + 0 + + OpenGL + SDL + OpenGL then SDL + SDL then OpenGL + + + + + False + True + 1 + + + + + True + False + + + True + False + Flavor + + + 0 + 0 + + + + + True + False + compatibility + + Legacy (1.1+) + Compatibility (2.1+) + Core (3.0+) + GLES (2.0+) + + + + + 1 + 0 + + + + + True + False + Version + + + 0 + 1 + + + + + True + False + Double Buffer + + + 0 + 2 + + + + + Enabled + True + True + False + 0 + True + True + + + + 1 + 2 + + + + + True + True + 3 + 2.1 + + + + 1 + 1 + + + + + False + True + 2 + + + + + True + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + True + 3 + + + + + False + True + 1 + + + + + True + False + vertical + + + True + False + FPS Limit + + + False + True + 0 + + + + + True + True + -1 + number + + + + False + True + 1 + + + + + False + True + 2 + + + + + Enable _Background + True + True + False + True + 0 + True + True + + + + False + True + 3 + + + + + Enable _Particles + True + True + False + True + 0 + True + True + + + + False + True + 4 + + + + + Enable _Sound + True + True + False + True + 0 + True + True + + + + False + True + 5 + + + + + Back + True + True + True + + + + False + True + 6 + + + + + + + + *.rpy + + + + False + PyTouhou + dialog + False + replay_filefilter + + + + + + + + False + vertical + 2 + + + False + end + + + gtk-cancel + True + True + True + True + True + + + + True + True + 0 + + + + + gtk-open + True + True + True + True + True + + + + True + True + 1 + + + + + False + True + end + 0 + + + + + + + True + False + PyTouhou + False + False + + + + + True + False + vertical + start + + + Start + True + True + True + + + + True + True + 0 + + + + + Extra Start + True + True + True + + + + True + True + 1 + + + + + Practice Start + True + True + True + + + + True + True + 2 + + + + + Netplay Start + True + False + True + True + + + + True + True + 3 + + + + + Replay + True + True + True + + + + True + True + 4 + + + + + Score + True + False + True + True + + + + True + True + 5 + + + + + Music Room + True + False + True + True + + + + True + True + 6 + + + + + Options + True + True + True + + + + True + True + 7 + + + + + Quit + True + True + True + + + + True + True + 8 + + + + + + diff --git a/pytouhou/menu.py b/pytouhou/menu.py new file mode 100644 --- /dev/null +++ b/pytouhou/menu.py @@ -0,0 +1,305 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2014 Emmanuel Gil Peyrot +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +from pytouhou.utils.helpers import get_logger +logger = get_logger(__name__) + +try: + from gi.repository import Gtk, Gdk +except ImportError: + logger.error('GTK+ unavailable, disabling the GUI menu.') + raise + +import sys +import re + +GL_VERSION_REGEX = re.compile(r'^\d\.\d$') + + +class Handler(object): + def __init__(self, config, args): + self.config = config + self.args = args + + def init_gtk(self, builder): + self.start_window = builder.get_object('start_window') + self.game_window = builder.get_object('game_window') + self.options_window = builder.get_object('options_window') + + self.replay_filechooserdialog = builder.get_object('replay_filechooserdialog') + + # Game widgets + self.difficulty_box = builder.get_object('difficulty_box') + self.character_box = builder.get_object('character_box') + self.stage_box = builder.get_object('stage_box') + self.boss_rush_checkbutton = builder.get_object('boss_rush_checkbutton') + + self.difficulty_comboboxtext = builder.get_object('difficulty_comboboxtext') + self.character_comboboxtext = builder.get_object('character_comboboxtext') + self.stage_comboboxtext = builder.get_object('stage_comboboxtext') + + # Options widgets + self.path_filechooserbutton = builder.get_object('path_filechooserbutton') + self.backend_comboboxtext = builder.get_object('backend_comboboxtext') + + # OpenGL backend + self.opengl_grid = builder.get_object('opengl_grid') + self.flavor_comboboxtext = builder.get_object('flavor_comboboxtext') + self.version_entry = builder.get_object('version_entry') + self.double_buffer_checkbutton = builder.get_object('double_buffer_checkbutton') + + # SDL backend + self.sdl_grid = builder.get_object('sdl_grid') + + self.fps_entry = builder.get_object('fps_entry') + self.no_background_checkbutton = builder.get_object('no_background_checkbutton') + self.no_particles_checkbutton = builder.get_object('no_particles_checkbutton') + self.no_sound_checkbutton = builder.get_object('no_sound_checkbutton') + + self.difficulty_comboboxtext.set_active_id(str(self.args.rank)) + self.character_comboboxtext.set_active_id(str(self.args.character)) + self.stage_comboboxtext.set_active_id(str(self.args.stage)) + self.boss_rush_checkbutton.set_active(self.args.boss_rush) + + self.path_filechooserbutton.set_filename(self.args.path) + self.backend_comboboxtext.set_active_id(' '.join(self.args.backend)) + + self.flavor_comboboxtext.set_active_id(self.args.gl_flavor) + self.version_entry.set_text(str(self.args.gl_version)) + if self.args.double_buffer is None: + self.double_buffer_checkbutton.set_inconsistent(True) + else: + self.double_buffer_checkbutton.set_inconsistent(False) + self.double_buffer_checkbutton.set_active(self.args.double_buffer) + + self.fps_entry.set_text(str(self.args.fps_limit)) + self.no_background_checkbutton.set_active(self.args.no_background) + self.no_particles_checkbutton.set_active(self.args.no_particles) + self.no_sound_checkbutton.set_active(self.args.no_sound) + + def hide_and_play(self, window): + window.hide() + Gtk.main_quit() + print('Play!') + + def on_quit(self, *args): + Gtk.main_quit(*args) + sys.exit(0) + + + # Main menu + + def on_start_window_key_press_event(self, window, event_key): + if (event_key.keyval == Gdk.KEY_Escape or + event_key.state == Gdk.ModifierType.CONTROL_MASK and event_key.keyval == Gdk.KEY_q): + self.on_quit() + + def on_start_button_clicked(self, _): + self.difficulty_box.show() + self.character_box.show() + self.stage_box.hide() + + self.stage_comboboxtext.set_active_id(None) + self.args.stage = None + + self.start_window.hide() + self.game_window.show() + + def on_extra_start_button_clicked(self, _): + self.difficulty_box.hide() + self.character_box.show() + self.stage_box.hide() + + self.difficulty_comboboxtext.set_active_id('4') + self.stage_comboboxtext.set_active_id('7') + + self.start_window.hide() + self.game_window.show() + + def on_practice_start_button_clicked(self, _): + self.difficulty_box.show() + self.character_box.show() + self.stage_box.show() + + self.start_window.hide() + self.game_window.show() + + def on_options_button_clicked(self, _): + self.start_window.hide() + self.options_window.show() + + def on_replay_button_clicked(self, _): + self.start_window.hide() + self.replay_filechooserdialog.show() + + def on_inactive_button_clicked(self, _): + raise NotImplementedError('Menu not implemented') + + on_netplay_start_button_clicked = on_inactive_button_clicked + on_score_button_clicked = on_inactive_button_clicked + on_music_room_button_clicked = on_inactive_button_clicked + + + # Game menu + + def on_game_back_button_clicked(self, _): + self.game_window.hide() + self.start_window.show() + + def on_play_button_clicked(self, _): + self.hide_and_play(self.game_window) + + def on_game_window_key_press_event(self, window, event_key): + if event_key.keyval == Gdk.KEY_Escape: + self.game_window.hide() + self.start_window.show() + elif event_key.state == Gdk.ModifierType.CONTROL_MASK and event_key.keyval == Gdk.KEY_q: + self.on_quit() + + def on_difficulty_comboboxtext_changed(self, item): + active = item.get_active_id() + difficulty = int(active) + self.config.set('rank', active if difficulty < 4 else None) + self.args.rank = difficulty + + def on_character_comboboxtext_changed(self, item): + character = int(item.get_active_id()) + self.config.set('character', character) + self.args.character = character + + def on_stage_comboboxtext_changed(self, item): + stage = item.get_active_id() + self.args.stage = int(stage) if stage is not None else None + + def on_boss_rush_checkbutton_toggled(self, boss_rush_checkbutton): + active = boss_rush_checkbutton.get_active() + self.config.set('boss-rush', active) + self.args.boss_rush = active + + + # Replay dialog + + def on_replay_filechooserdialog_close(self, _): + self.replay_filechooserdialog.hide() + self.start_window.show() + + on_replay_cancel_button_clicked = on_replay_filechooserdialog_close + + def on_replay_open_button_clicked(self, button): + try: + open(self.args.replay, 'rb').close() + except (IOError, TypeError): + return + self.hide_and_play(self.replay_filechooserdialog) + + def on_replay_filechooserdialog_selection_changed(self, dialog): + self.args.replay = dialog.get_filename() + + def on_replay_filechooserdialog_file_activated(self, window): + self.args.replay = window.get_filename() + self.hide_and_play(self.replay_filechooserdialog) + + def on_replay_filechooserdialog_key_press_event(self, window, event_key): + if event_key.keyval == Gdk.KEY_Escape: + self.replay_filechooserdialog.hide() + self.start_window.show() + elif event_key.state == Gdk.ModifierType.CONTROL_MASK and event_key.keyval == Gdk.KEY_q: + self.on_quit() + + + # Options menu + + def on_options_back_button_clicked(self, _): + self.options_window.hide() + self.start_window.show() + + def on_options_window_key_press_event(self, window, event_key): + if event_key.keyval == Gdk.KEY_Escape: + self.options_window.hide() + self.start_window.show() + elif event_key.state == Gdk.ModifierType.CONTROL_MASK and event_key.keyval == Gdk.KEY_q: + self.on_quit() + + def on_path_filechooserbutton_file_set(self, path_filechooserbutton): + path = path_filechooserbutton.get_filename() + self.config.set('path', path) + + def on_backend_comboboxtext_changed(self, backend_comboboxtext): + active = backend_comboboxtext.get_active_id() + self.config.set('backend', active) + backends = active.split() + new_grids = [getattr(self, backend + '_grid') for backend in backends] + for grid in [self.opengl_grid, self.sdl_grid]: + if grid not in new_grids: + grid.hide() + else: + grid.show() + + def on_flavor_comboboxtext_changed(self, flavor_comboboxtext): + active = flavor_comboboxtext.get_active_id() + self.config.set('gl-flavor', active) + + def on_version_entry_changed(self, version_entry): + text = version_entry.get_text() + if not GL_VERSION_REGEX.match(text): + raise ValueError('ā€œ%sā€ is not .' % text) + self.config.set('gl-version', text) + + def on_double_buffer_checkbutton_clicked(self, double_buffer_checkbutton): + inconsistent = double_buffer_checkbutton.get_inconsistent() + active = double_buffer_checkbutton.get_active() + if inconsistent: + active = False + inconsistent = False + elif active: + active = True + else: + inconsistent = True + double_buffer_checkbutton.set_active(active) + double_buffer_checkbutton.set_inconsistent(inconsistent) + self.config.set('double-buffer', None if inconsistent else active) + + def on_fps_entry_changed(self, fps_entry): + text = fps_entry.get_text() + try: + int(text) + except ValueError: + raise ValueError('ā€œ%sā€ is not integer' % text) + else: + self.config.set('fps-limit', text) + + def on_no_background_checkbutton_toggled(self, checkbutton): + active = checkbutton.get_active() + self.config.set('no-background', not active) + + def on_no_particles_checkbutton_toggled(self, checkbutton): + active = checkbutton.get_active() + self.config.set('no-particles', active) + + def on_no_sound_checkbutton_toggled(self, checkbutton): + active = checkbutton.get_active() + self.config.set('no-sound', active) + + +def menu(config, args): + assert Gtk + handler = Handler(config, args) + + builder = Gtk.Builder() + builder.add_from_file('data/menu.glade') + builder.connect_signals(handler) + + handler.init_gtk(builder) + + Gtk.main() diff --git a/pytouhou/options.py b/pytouhou/options.py --- a/pytouhou/options.py +++ b/pytouhou/options.py @@ -15,7 +15,7 @@ import os from ConfigParser import RawConfigParser, NoOptionError -from pytouhou.utils.xdg import load_config_paths +from pytouhou.utils.xdg import load_config_paths, save_config_path class Options(object): @@ -23,6 +23,7 @@ class Options(object): load_paths = list(reversed([os.path.join(directory, '%s.cfg' % name) for directory in load_config_paths(name)])) + self.save_path = os.path.join(save_config_path(name), '%s.cfg' % name) self.config = RawConfigParser(defaults) self.paths = self.config.read(load_paths) @@ -34,6 +35,18 @@ class Options(object): except NoOptionError: return None + def set(self, option, value): + if value is not None: + self.config.set(self.section, option, value) + else: + self.config.remove_option(self.section, option) + + defaults = self.config._defaults + self.config._defaults = None + with open(self.save_path, 'w') as save_file: + self.config.write(save_file) + self.config._defaults = defaults + def patch_argument_parser(): from argparse import ArgumentParser, _ActionsContainer @@ -105,6 +118,7 @@ def parse_arguments(defaults): parser.add_argument('-p', '--path', metavar='DIRECTORY', help='Game directory path.') parser.add_argument('--debug', action='store_true', help='Set unlimited continues, and perhaps other debug features.') parser.add_argument('--verbosity', metavar='VERBOSITY', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], help='Select the wanted logging level.') + parser.add_argument('--no-menu', action='store_true', help='Disable the menu.') game_group = parser.add_argument_group('Game options') game_group.add_argument('-s', '--stage', metavar='STAGE', type=int, help='Stage, 1 to 7 (Extra), nothing means story mode.') diff --git a/scripts/pytouhou b/scripts/pytouhou --- a/scripts/pytouhou +++ b/scripts/pytouhou @@ -48,6 +48,10 @@ logger.root.name = 'pytouhou' logger.info('Configuration loaded from: %s', ', '.join(options.paths)) +if not args.no_menu: + from pytouhou.menu import menu + menu(options, args) + if args.game == 'EoSD': from pytouhou.games.eosd import EoSDCommon as Common, EoSDGame as Game