"""Widget generators and their signal handlers""" import os # Standard Library # pylint: disable=no-member,too-many-public-methods from gettext import gettext as _ from typing import Any, Dict, Optional # Third Party Libraries from gi.repository import Gtk, Pango # Lutris Modules from lutris import settings, sysoptions from lutris.config import LutrisConfig from lutris.game import Game from lutris.gui.config.widget_generator import WidgetGenerator from lutris.gui.widgets.common import VBox from lutris.runners import InvalidRunnerError, import_runner from lutris.util.log import logger from lutris.util.wine.wine import clear_wine_version_cache def set_option_wrapper_style_class(wrapper: Gtk.Widget, class_name: Optional[str]): """Sets a particular CSS class on a wrapper, and removes any other classes that start with 'option-wrapper-' so there's only one o these classes.""" style_context = wrapper.get_style_context() for cls in style_context.list_classes(): if cls.startswith("option-wrapper-") and cls != class_name: style_context.remove_class(cls) if class_name: style_context.add_class(class_name) class ConfigBox(VBox): """Dynamically generate a vbox built upon on a python dict.""" config_section = NotImplemented def __init__(self, config_level: str, lutris_config: LutrisConfig, game: Game = None, **kwargs) -> None: super().__init__(**kwargs) self.options = [] self.config_level = config_level self.lutris_config = lutris_config self.game = game self.config = None self.raw_config = None self.files = [] self.files_list_store = None self._widget_generator = None self._advanced_visibility = False self._filter = "" self._filter_text = "" self.no_options_label = Gtk.Label(halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER) self.no_options_label.set_line_wrap(True) self.no_options_label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) self.pack_end(self.no_options_label, True, True, 0) @property def advanced_visibility(self): return self._advanced_visibility @advanced_visibility.setter def advanced_visibility(self, value): """Sets the visibility of every 'advanced' option and every section that contains only 'advanced' options.""" self._advanced_visibility = value self.update_widgets() @property def filter(self) -> str: return self._filter @filter.setter def filter(self, value: str) -> None: """Sets the visibility of the options that have some text in the label or help-text.""" self._filter = value self._filter_text = value.casefold() self.update_widgets() def generate_top_info_box(self, text): """Add a top section with general help text for the current tab""" help_box = Gtk.Box() help_box.set_margin_left(15) help_box.set_margin_right(15) help_box.set_margin_bottom(5) icon = Gtk.Image.new_from_icon_name("dialog-information", Gtk.IconSize.MENU) help_box.pack_start(icon, False, False, 5) title_label = Gtk.Label("%s" % text) title_label.set_line_wrap(True) title_label.set_alignment(0, 0.5) title_label.set_use_markup(True) help_box.pack_start(title_label, False, False, 5) self.pack_start(help_box, False, False, 0) self.pack_start(Gtk.HSeparator(), False, False, 12) help_box.show_all() def get_widget_generator(self) -> "ConfigWidgetGenerator": """Returns an object that creates option widgets and tracks them; this is lazy-initialized, but repeated calls return the same generator.""" if self._widget_generator: return self._widget_generator gen = ConfigWidgetGenerator(self) if self.game and self.game.directory: gen.default_directory = self.game.directory elif self.game and self.game.has_runner: gen.default_directory = self.game.runner.working_dir elif self.lutris_config: gen.default_directory = self.lutris_config.system_config.get("game_path") or os.path.expanduser("~") else: gen.default_directory = os.path.expanduser("~") self._widget_generator = gen return gen def filter_widget(self, option_container: Gtk.Widget) -> bool: """Called by the widget generate to filter option containers; return true for those that should be visible.""" if not self.advanced_visibility: is_advanced = hasattr(option_container, "lutris_advanced") and option_container.lutris_advanced if is_advanced: # Record that we hid this because it was advanced, not because of ordinary # visibility option_container.lutris_advanced_hidden = True return False filter_text = self._filter_text if filter_text and hasattr(option_container, "lutris_option_label"): label = option_container.lutris_option_label.casefold() helptext = option_container.lutris_option_helptext.casefold() if filter_text not in label and filter_text not in helptext: return False return True def generate_widgets(self): """Parse the config dict and generates widget accordingly.""" # Select config section. if self.config_section == "game": self.config = self.lutris_config.game_config self.raw_config = self.lutris_config.raw_game_config elif self.config_section == "runner": self.config = self.lutris_config.runner_config self.raw_config = self.lutris_config.raw_runner_config elif self.config_section == "system": self.config = self.lutris_config.system_config self.raw_config = self.lutris_config.raw_system_config gen = self.get_widget_generator() # Go thru all options. for option in self.options: try: if "scope" in option: if self.config_level not in option["scope"]: continue # Generate option widget gen.add_container(option) except Exception as ex: logger.exception("Failed to generate option widget for '%s': %s", option.get("option"), ex) show_advanced = settings.read_setting("show_advanced_options") == "True" self._advanced_visibility = show_advanced gen.update_widgets() def update_widgets(self): if self._widget_generator: self._widget_generator.update_widgets() class GameBox(ConfigBox): config_section = "game" def __init__(self, config_level: str, lutris_config: LutrisConfig, game: Game, **kwargs): ConfigBox.__init__(self, config_level, lutris_config, game, **kwargs) self.runner = game.runner if self.runner: self.options = self.runner.game_options else: logger.warning("No runner in game supplied to GameBox") class RunnerBox(ConfigBox): """Configuration box for runner specific options""" config_section = "runner" def __init__(self, config_level: str, lutris_config: LutrisConfig, game: Game = None, **kwargs): ConfigBox.__init__(self, config_level, lutris_config, game, **kwargs) try: self.runner = import_runner(self.lutris_config.runner_slug)() except InvalidRunnerError: self.runner = None if self.runner: self.options = self.runner.get_runner_options() if lutris_config.level == "game": self.generate_top_info_box( _("If modified, these options supersede the same options from " "the base runner configuration.") ) def generate_widgets(self): # Better safe than sorry - we search of Wine versions in directories # we do not control, so let's keep up to date more aggresively. clear_wine_version_cache() return super().generate_widgets() class SystemConfigBox(ConfigBox): config_section = "system" def __init__(self, config_level: str, lutris_config: LutrisConfig, **kwargs) -> None: ConfigBox.__init__(self, config_level, lutris_config, **kwargs) self.runner = None runner_slug = self.lutris_config.runner_slug if runner_slug: self.options = sysoptions.with_runner_overrides(runner_slug) else: self.options = sysoptions.system_options if lutris_config.game_config_id and runner_slug: self.generate_top_info_box( _( "If modified, these options supersede the same options from " "the base runner configuration, which themselves supersede " "the global preferences." ) ) elif runner_slug: self.generate_top_info_box( _("If modified, these options supersede the same options from " "the global preferences.") ) class ConfigWidgetGenerator(WidgetGenerator): def __init__(self, parent: ConfigBox) -> None: super().__init__(parent, parent.lutris_config) if parent.config is None or parent.raw_config is None: raise RuntimeError("Widgets can't be generated before the config is initialized.") self.config = parent.config self.raw_config = parent.raw_config self.lutris_config = parent.lutris_config self.reset_buttons: Dict[str, Gtk.Button] = {} def get_setting(self, option_key: str, default: Any) -> Any: if option_key in self.config: return self.config.get(option_key) else: return default def update_option_container(self, option, container: Gtk.Box, wrapper: Gtk.Box): super().update_option_container(option, container, wrapper) option_key = option["option"] if option_key in self.raw_config: set_option_wrapper_style_class(wrapper, "option-wrapper-assigned-here") else: default = self.get_default(option) value = self.get_setting(option_key, default) if value != default: set_option_wrapper_style_class(wrapper, "option-wrapper-non-default") else: set_option_wrapper_style_class(wrapper, None) def create_option_container(self, option: Dict[str, Any], wrapper: Gtk.Widget) -> Gtk.Widget: option_key = option["option"] reset_container = Gtk.Box(visible=True) reset_container.set_margin_left(18) reset_container.pack_start(wrapper, True, True, 0) reset_button = Gtk.Button.new_from_icon_name("edit-undo-symbolic", Gtk.IconSize.MENU) reset_button.get_style_context().add_class("reset-button") reset_button.set_valign(Gtk.Align.CENTER) reset_button.set_halign(Gtk.Align.CENTER) reset_button.set_margin_bottom(6) reset_button.set_relief(Gtk.ReliefStyle.NONE) reset_button.set_tooltip_text(_("Reset option to global or default config")) reset_button.connect("clicked", self.on_reset_button_clicked, option) self.reset_buttons[option_key] = reset_button if option_key not in self.raw_config: reset_button.set_visible(False) reset_button.set_no_show_all(True) placeholder = Gtk.Box() placeholder.set_size_request(28, -1) placeholder.pack_start(reset_button, False, False, 0) reset_container.pack_end(placeholder, False, False, 5) return super().create_option_container(option, reset_container) def get_visibility(self, option: Dict[str, Any]) -> bool: option_container = self.option_containers[option["option"]] option_container.lutris_advanced_hidden = False option_visibility = super().get_visibility(option) if not option_visibility: return False return self.parent.filter_widget(option_container) def get_tooltip(self, option: Dict[str, Any], value: Any, default: Any): tooltip = super().get_tooltip(option, value, default) option_key = option["option"] if value != default and option_key not in self.raw_config: tooltip = tooltip + "\n\n" if tooltip else "" tooltip += _("(Italic indicates that this option is modified in a lower configuration level.)") return tooltip def update_widgets(self): super().update_widgets() def get_no_options_message() -> Optional[str]: if self.option_containers: for container in self.option_containers.values(): if container.get_visible(): return None if self.parent.filter: return _("No options match '%s'") % self.parent.filter elif any(c.lutris_advanced_hidden for c in self.option_containers.values()): return _("Only advanced options available") return _("No options available") message = get_no_options_message() if message: self.parent.no_options_label.set_text(message) self.parent.no_options_label.show() else: self.parent.no_options_label.hide() def on_reset_button_clicked(self, btn, option): """Clear option (remove from config, reset option widget).""" option_key = option["option"] wrapper = self.wrappers[option_key] btn.set_visible(False) self.raw_config.pop(option_key, None) self.lutris_config.update_cascaded_config() self.generate_widget(option, wrapper=wrapper) self.update_widgets() def on_changed(self, option_key, new_value): """Common actions when value changed on a widget""" self.raw_config[option_key] = new_value self.config[option_key] = new_value reset_btn = self.reset_buttons.get(option_key) if reset_btn: reset_btn.set_visible(True) super().on_changed(option_key, new_value)