"""Widget generators and their signal handlers""" import os from abc import ABC, abstractmethod from gettext import gettext as _ from inspect import Parameter, signature from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional from gi.repository import Gdk, Gtk # type: ignore from lutris.config import LutrisConfig from lutris.gui.widgets import NotificationSource from lutris.gui.widgets.common import EditableGrid, FileChooserEntry, Label from lutris.gui.widgets.searchable_entrybox import SearchableEntrybox from lutris.util.log import logger from lutris.util.strings import gtk_safe if TYPE_CHECKING: from lutris.gui.config.boxes import ConfigBox class WidgetGenerator(ABC): """This class generates widgets for an options page. It even adds the widgets to a 'wrapper' you can supply and, if required, that wrapper goes into a container which also contains message boxes for errors and warnings. The specific inner widgets are generated according to an option dict. The generator also accumulates and tracks the wrappers and containers for future use, and can update their state via a call to update_widgets(). When doing this, various callables in the option dicts can be re-evaluated; you provide a set of 'callback_args' and 'callback_kwargs' that are passed along to these callbacks. These collbacks also take the identifier of the option, from the 'option' key of the option dict. 'changed' is a NotificationSource for whenever the generated widget changes a value; the same 'changed' NotificationSource is shared by all the widgets this generator generates. You can only create new containers, not discard old ones, but you *can* regenerate the widget inside the wrapper as a way to force it to update. """ GeneratorFunction = Callable[[Dict[str, Any], Any, Any], Optional[Gtk.Widget]] def __init__(self, parent: "ConfigBox", *callback_args, **callback_kwargs) -> None: self.parent = parent self.callback_args = callback_args self.callback_kwargs = callback_kwargs self.changed = NotificationSource() # takes option_key, new_value self.changed.register(self.on_changed, priority=1000) self._default_directory: Optional[str] = None self._current_parent: Optional[Gtk.Box] = None self._current_section: Optional[str] = None # These are outputs set by generate_widget() or generate_container() # and they are reset on each call. self.wrapper: Optional[Gtk.Box] = None self.default_value = None self.tooltip_default: Optional[str] = None self.options: Dict[str, Dict[str, Any]] = {} self.option_widget: Optional[Gtk.Widget] = None self.option_container: Optional[Gtk.Widget] = None self.warning_messages: List[Gtk.Widget] = [] # These accumulate results across all widgets self.wrappers: Dict[str, Gtk.Container] = {} self.section_frames: List[SectionFrame] = [] self.option_containers: Dict[str, Gtk.Container] = {} self._generators: Dict[str, WidgetGenerator.GeneratorFunction] = { "label": self._generate_label, "string": self._generate_string, "bool": self._generate_bool, "range": self._generate_range, "choice": self._generate_choice, "choice_with_entry": self._generate_choice_with_entry, "choice_with_search": self._generate_choice_with_search, "file": self._generate_file, "multiple_file": self._generate_multiple_file, "directory": self._generate_directory, "mapping": self._generate_mapping, "command_line": self._generate_command_line, } def on_changed(self, option_key: str, new_value: Any) -> None: """Called when any value is changed; this is called later than ordinary handlers for the 'changed' notification, and by default just updates the widgets.""" self.update_widgets() @property def default_directory(self) -> str: """This is the directory selected by default by file and directory choosers.""" if not self._default_directory: lutris_config = LutrisConfig() self._default_directory = lutris_config.system_config.get("game_path") or os.path.expanduser("~") return self._default_directory @default_directory.setter def default_directory(self, new_dir: str) -> None: self._default_directory = new_dir # Widget Construction def add_container(self, option: Dict[str, Any], wrapper: Optional[Gtk.Box] = None) -> Optional[Gtk.Widget]: """Generates the option's widget, wrapper and container, and adds the container to the parent; if the option uses 'section', then the container is actually placed inside a SectionFrame, or in the previous frame if it is for the same section.""" option_container = self.generate_container(option, wrapper) if option_container and self.parent: # Switch to new section if required if not self._current_parent: self._current_parent = self.parent if option.get("section") != self._current_section: self._current_section = option.get("section") if self._current_section: frame = SectionFrame(self._current_section, visible=True) self.section_frames.append(frame) self._current_parent = frame.vbox self.parent.pack_start(frame, False, False, 0) else: self._current_parent = self.parent self._current_parent.pack_start(option_container, False, False, 0) return option_container def generate_container(self, option: Dict[str, Any], wrapper: Optional[Gtk.Box] = None) -> Optional[Gtk.Widget]: """Creates the widget, wrapper, and container; this returns the container (or the wrapper if there's no container).""" option_widget = self.generate_widget(option, wrapper) if option_widget and self.wrapper: option_key = option["option"] option_container = self.create_option_container(option, self.wrapper) self.option_containers[option_key] = option_container option_container.show_all() option_container.lutris_option_key = option_key # type:ignore[attr-defined] option_container.lutris_option_label = option["label"] # type:ignore[attr-defined] option_container.lutris_option_helptext = option.get("help") or "" # type:ignore[attr-defined] # Mark advanced option containers, to be hidden by checking for this option_container.lutris_advanced = bool(option.get("advanced")) # type:ignore[attr-defined] option_container.lutris_option = option # type:ignore[attr-defined] self.option_container = option_container return option_container else: return None def generate_widget(self, option: Dict[str, Any], wrapper: Optional[Gtk.Box] = None) -> Optional[Gtk.Widget]: """This creates a wrapper box and a label and widget within it according to the options dict given. The option widget itself, is returned, but this method also sets attributes on the generator. You get 'wrapper', 'default_value', 'tooltip_default' and 'option_widget' which restates the return value. This returns None if the entire option should be omitted.""" option_key = option["option"] option_type = option["type"] default = self.get_default(option) value = self.get_setting(option_key, default) self.default_value = default self.tooltip_default = None self.option_widget = None self.option_container = None self.warning_messages.clear() self.wrappers.pop(option_key, None) # Record the options themselves before anything is generated self.options[option_key] = option if wrapper: # Destroy and recreate option widget children = wrapper.get_children() for child in children: child.destroy() self.wrapper = wrapper else: self.wrapper = self.create_wrapper_box(option, value, default) if not self.wrapper: return None func = self._generators.get(option_type) if func: option_widget = func(option, value, default) else: raise ValueError("Unknown widget type %s" % option_type) self.wrappers[option_key] = self.wrapper self.option_widget = option_widget self.tooltip_default = self.tooltip_default or (default if isinstance(default, str) else None) if option_widget: option_widget.show_all() self.configure_wrapper_box(self.wrapper, option, value, default) self.configure_warning_messages(option) return option_widget def configure_wrapper_box(self, wrapper: Gtk.Widget, option: Dict[str, Any], value: Any, default: Any) -> None: """Configures the wrapper box after it is created; this sets its tooltip, sensitivity, and creates warning message boxes.""" # Attach a tooltip to the wrapper tooltip = self.get_tooltip(option, value, default) if tooltip: wrapper.props.has_tooltip = True wrapper.connect("query-tooltip", self.on_query_tooltip, tooltip) def get_tooltip(self, option: Dict[str, Any], value: Any, default: Any): tooltip = option.get("help") if self.tooltip_default: tooltip = tooltip + "\n\n" if tooltip else "" tooltip += _("Default: ") + self.tooltip_default return tooltip def configure_warning_messages(self, option: Dict[str, Any]): # Add message boxes under the widget if "error" in option: self.warning_messages.append(ConfigErrorBox(option["error"])) if "warning" in option: self.warning_messages.append(ConfigWarningBox(option["warning"])) def create_wrapper_box(self, option: Dict[str, Any], value: Any, default: Any) -> Optional[Gtk.Box]: """This creates the wrapper, which becomes the 'wrapper' attribute and which build_option_widget() populates. Returns None if the option is not visible; in that case no widget is generated either.""" available = self._evaluate_flag_option("available", option) if not available: # If not available, there's no wrapper, and no widget! return None return Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12, margin_bottom=6, visible=True) def create_option_container(self, option: Dict[str, Any], wrapper: Gtk.Container) -> Gtk.Container: """This creates a wrapper box around the widget wrapper, to support additional controls. The base implementation wraps 'wrapper' in a Box with the error and warning widgets; if there are none it just returns 'wrapper'.""" if self.warning_messages: option_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, visible=True) option_container.pack_start(wrapper, False, False, 0) for widget in self.warning_messages: option_container.pack_start(widget, False, False, 0) return option_container else: return wrapper def build_option_widget( self, option: Dict[str, Any], widget: Optional[Gtk.Widget], no_label: bool = False, expand: bool = True ) -> Optional[Gtk.Widget]: """This is called by the generator methods to place their widget into the wrapper, usually with a label taken from 'option'. The default labeling and placement is suitable for our ConfigBoxes, but we override this to get a different layout for PreferencesBox. Some generators do their own labelling, and pass True for no_label; this method will pack the widget with no label in that case. This method returns the widget.""" if self.wrapper: if not no_label and "label" in option: label = option["label"] label = Label(label) self.wrapper.pack_start(label, False, False, 0) if widget: option_size = option.get("size", None) if option_size: expand = option_size != "small" self.wrapper.pack_start(widget, expand, expand, 0) return widget # Dynamic Widget Updates def update_widgets(self) -> None: """Call this to update the visibility, sensitivity and other properties of the widgets, wrappers and containers already generated.""" for option_key, container in self.option_containers.items(): if hasattr(container, "lutris_option"): option = container.lutris_option wrapper = self.wrappers[option_key] self.update_option_container(option, container, wrapper) for frame in self.section_frames: visible = frame.has_visible_children() frame.set_visible(visible) frame.set_no_show_all(not visible) def update_option_container(self, option, container: Gtk.Container, wrapper: Gtk.Container): """This method updates an option container and its wrapper; this re-evaluates the relevant options in case they contain callables and those callables return different results.""" # Update messages in message boxes that support it for child in container.get_children(): if hasattr(child, "update_message"): child.update_message(option, self) # Hide entire container if the option is not visible visible = self.get_visibility(option) container.set_visible(visible) container.set_no_show_all(not visible) # Grey out option if condition unmet, or if a second setting is False condition: bool = self.get_condition(option) wrapper.set_sensitive(condition) # Widget factories # Label def _generate_label(self, option, value, default): """Generate a simple label.""" text = option["label"] label = Label(text) label.set_use_markup(True) label.set_halign(Gtk.Align.START) label.set_valign(Gtk.Align.CENTER) return self.build_option_widget(option, label, no_label=True) # Entry def _generate_string(self, option, value, default): """Generate an entry box.""" def on_changed(entry): """Action triggered for entry 'changed' signal.""" self.changed.fire(option_key, entry.get_text()) option_key = option["option"] entry = Gtk.Entry() entry.set_text(value or default or "") entry.connect("changed", on_changed) return self.build_option_widget(option, entry) # Switch def _generate_bool(self, option, value, default): """Generate a switch.""" def on_notify_active(widget, _gparam): """Action for the switch's toggled signal.""" self.changed.fire(option_key, widget.get_active()) def to_bool(to_convert): """Convert values to booleans in a way that won't decide that the string 'False' is True!""" if to_convert is None: return None elif isinstance(to_convert, str): text = to_convert.casefold().strip() if text == "true": return True elif text == "false": return False else: return None else: return bool(to_convert) option_key = option["option"] active = to_bool(value) if active is None: active = bool(to_bool(default)) switch = Gtk.Switch(active=active, valign=Gtk.Align.CENTER) switch.connect("notify::active", on_notify_active) self.tooltip_default = _("Enabled") if default else _("Disabled") return self.build_option_widget(option, switch, expand=False) # SpinButton def _generate_range(self, option, value, default): """Generate a ranged spin button.""" def on_changed(widget): """Action triggered on spin button 'changed' signal.""" new_value = widget.get_value_as_int() self.changed.fire(option_key, new_value) option_key = option["option"] min_val = option["min"] max_val = option["max"] adjustment = Gtk.Adjustment(float(min_val), float(min_val), float(max_val), 1, 0, 0) spin_button = Gtk.SpinButton() spin_button.set_adjustment(adjustment) spin_button.set_value(value or default or 0) spin_button.connect("changed", on_changed) return self.build_option_widget(option, spin_button) # ComboBox def _generate_choice(self, option, value, default, has_entry=False): """Generate a combobox (drop-down menu).""" def populate_combobox_choices(): expanded, tooltip_default, _valid_choices = expand_combobox_choices() for choice in expanded: liststore.append(choice) if tooltip_default: self.tooltip_default = str(tooltip_default) def expand_combobox_choices(): expanded = [] tooltip_default = None valid = [] has_value = False for choice in choices: if isinstance(choice, str): choice = (choice, choice) if choice[1] == value: has_value = True if choice[1] == default: tooltip_default = choice[0] choice = (_("%s (default)") % choice[0], choice[1]) valid.append(choice[1]) expanded.append(choice) if not has_value and value: expanded.insert(0, (value, value)) return expanded, tooltip_default, valid def on_combobox_scroll(widget, _event): """Prevents users from accidentally changing configuration values while scrolling down dialogs. """ widget.stop_emission_by_name("scroll-event") return False def on_combobox_change(widget): """Action triggered on combobox 'changed' signal.""" list_store = widget.get_model() active = widget.get_active() option_value = None if active < 0: if widget.get_has_entry(): option_value = widget.get_child().get_text() else: option_value = list_store[active][1] self.changed.fire(option_key, option_value) option_key = option["option"] choices = self._evaluate_option("choices", None, option) liststore = Gtk.ListStore(str, str) populate_combobox_choices() # With entry ("choice_with_entry" type) if has_entry: combobox = Gtk.ComboBox.new_with_model_and_entry(liststore) combobox.set_entry_text_column(0) # No entry ("choice" type) else: combobox = Gtk.ComboBox.new_with_model(liststore) cell = Gtk.CellRendererText() combobox.pack_start(cell, True) combobox.add_attribute(cell, "text", 0) combobox.set_id_column(1) expanded_choices, _tooltip_default, valid_choices = expand_combobox_choices() if value in [v for _k, v in expanded_choices]: combobox.set_active_id(value) elif has_entry: for ch in combobox.get_children(): if isinstance(ch, Gtk.Entry): ch.set_text(value or "") break else: combobox.set_active_id(default) combobox.connect("changed", on_combobox_change) combobox.connect("scroll-event", on_combobox_scroll) combobox.set_valign(Gtk.Align.CENTER) def get_invalidity_error(key: str): v = self.get_setting(key, self.get_default(option)) if v in valid_choices: return None return _("The setting '%s' is no longer available. You should select another choice.") % v if not has_entry and value not in valid_choices: self.warning_messages.append(ConfigWarningBox(get_invalidity_error)) return self.build_option_widget(option, combobox) # ComboBox def _generate_choice_with_entry(self, option, value, default): return self._generate_choice(option, value, default, has_entry=True) # Searchable Entry def _generate_choice_with_search(self, option, value, default): """Generate a searchable combo box""" def on_changed(_widget, new_value): self.changed.fire(option_key, new_value) option_key = option["option"] choices = option["choices"] entrybox = SearchableEntrybox(choices, value or default) entrybox.connect("changed", on_changed) return self.build_option_widget(option, entrybox) # FileChooserEntry def _generate_file(self, option, value, default, shell_quoting=False): """Generate a file chooser button to select a file.""" def on_changed(entry): """Action triggered when the field's content changes.""" text = entry.get_text() self.changed.fire(option_key, text) option_key = option["option"] warn_if_non_writable_parent = bool(option.get("warn_if_non_writable_parent")) if not value: value = default if "default_path" in option: lutris_config = LutrisConfig() chooser_default_path = lutris_config.system_config.get(option["default_path"]) else: chooser_default_path = self.default_directory file_chooser = FileChooserEntry( title=_("Select file"), action=Gtk.FileChooserAction.OPEN, warn_if_non_writable_parent=warn_if_non_writable_parent, text=value, default_path=chooser_default_path, shell_quoting=shell_quoting, ) if value: # If path is relative, complete with default directory if not os.path.isabs(value): value = os.path.expanduser(value) if not os.path.isabs(value): value = os.path.join(self.default_directory, value) file_chooser.entry.set_text(value) file_chooser.set_valign(Gtk.Align.CENTER) file_chooser.connect("changed", on_changed) return self.build_option_widget(option, file_chooser) # FileChooserEntry def _generate_command_line(self, option, value, default): return self._generate_file(option, value, default, shell_quoting=True) # TreeView def _generate_multiple_file(self, option, value, default): """Generate a multiple file selector.""" def on_add_files_clicked(_widget): """Create and run multi-file chooser dialog.""" dialog = Gtk.FileChooserNative.new( _("Select files"), None, Gtk.FileChooserAction.OPEN, _("_Add"), _("_Cancel"), ) dialog.set_select_multiple(True) files = [row[0] for row in files_list_store] first_file_dir = os.path.dirname(files[0]) if files else None dialog.set_current_folder(first_file_dir or self.default_directory) response = dialog.run() if response == Gtk.ResponseType.ACCEPT: for filename in dialog.get_filenames(): if filename not in files: files_list_store.append([filename]) files.append(filename) self.changed.fire(option_key, files) dialog.destroy() def on_files_treeview_keypress(treeview, event): """Action triggered when a row is deleted from the filechooser.""" if event.keyval == Gdk.KEY_Delete: selection = treeview.get_selection() (model, treepaths) = selection.get_selected_rows() for treepath in treepaths: treeiter = model.get_iter(treepath) model.remove(treeiter) files = [row[0] for row in files_list_store] self.changed.fire(option_key, files) option_key = option["option"] label = option["label"] files_list_store = Gtk.ListStore(str) vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) label = Label(label + ":") label.set_halign(Gtk.Align.START) button = Gtk.Button(label=_("Add Files")) button.connect("clicked", on_add_files_clicked) button.set_margin_left(10) vbox.pack_start(label, False, False, 5) vbox.pack_end(button, False, False, 0) if not value: value = default if value: if isinstance(value, str): files = [value] else: files = value else: files = [] for filename in files: files_list_store.append([filename]) cell_renderer = Gtk.CellRendererText() files_treeview = Gtk.TreeView(model=files_list_store) files_column = Gtk.TreeViewColumn(_("Files"), cell_renderer, text=0) files_treeview.append_column(files_column) files_treeview.connect("key-press-event", on_files_treeview_keypress) treeview_scroll = Gtk.ScrolledWindow() treeview_scroll.set_min_content_height(130) treeview_scroll.set_margin_left(10) treeview_scroll.set_shadow_type(Gtk.ShadowType.ETCHED_IN) treeview_scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) treeview_scroll.add(files_treeview) vbox.pack_start(treeview_scroll, True, True, 0) return self.build_option_widget(option, vbox, no_label=True) # FileChooserEntry def _generate_directory(self, option, value, default): """Generate a file chooser button to select a directory.""" def on_changed(entry): """Action triggered when the field's content changes.""" text = entry.get_text() self.changed.fire(option_key, text) option_key = option["option"] warn_if_non_writable_parent = bool(option.get("warn_if_non_writable_parent")) if not value: value = default directory_chooser = FileChooserEntry( title=_("Select folder"), action=Gtk.FileChooserAction.SELECT_FOLDER, warn_if_non_writable_parent=warn_if_non_writable_parent, text=value, default_path=self.default_directory if not value else None, ) directory_chooser.connect("changed", on_changed) directory_chooser.set_valign(Gtk.Align.CENTER) return self.build_option_widget(option, directory_chooser) # EditableGrid def _generate_mapping(self, option, value, default): """Adds an editable grid widget""" def on_changed(widget): values = dict(widget.get_data()) self.changed.fire(option_key, values) option_key = option["option"] value = value or default or {} try: value = list(value.items()) except AttributeError: logger.error("Invalid value of type %s passed to grid widget: %s", type(value), value) value = {} grid = EditableGrid(value, columns=["Key", "Value"]) grid.connect("changed", on_changed) return self.build_option_widget(option, grid) @staticmethod def on_query_tooltip(_widget, _x, _y, _keybmode, tooltip, text): # pylint: disable=unused-argument """Prepare a custom tooltip with a fixed width""" label = Label(text) label.set_use_markup(True) label.set_max_width_chars(60) event_box = Gtk.EventBox() event_box.add(label) event_box.show_all() tooltip.set_custom(event_box) return True # Option access @abstractmethod def get_setting(self, option_key: str, default: Any) -> Any: """Reads the current value for a specific setting; this method must be implemented by a subclass.""" raise NotImplementedError() def get_default(self, option: Dict[str, Any]) -> Any: """Returns the default value from the option; if it is callable, this calls it to get the actual default.""" return self._evaluate_option("default", default=None, option=option) def get_visibility(self, option: Dict[str, Any]) -> bool: """Extracts the 'visible' option; if the option is missing this returns True, and if it is callable this calls it. Subclasses can add further conditions.""" return self._evaluate_flag_option("visible", option) def get_condition(self, option: Dict[str, Any]) -> bool: """Extracts the 'condition' option; but also the 'conditional_on' option, and if both are present, then if either indicates the control should be disabled this will be false..""" condition = self._evaluate_flag_option("condition", option) conditional_on = option.get("conditional_on") if conditional_on: conditional_on_default = self.get_default(self.options[conditional_on]) if not self.get_setting(conditional_on, conditional_on_default): return False container = self.option_containers[option["option"]] for child in container.get_children(): if hasattr(child, "blocks_sensitivity") and child.blocks_sensitivity: return False return condition def _evaluate_flag_option(self, key: str, option: Dict[str, Any]) -> bool: """Evaluates a flag option; if is None or missing this returns True, and if it is callable this calls it (as with _evaluate_option) and converts the result to a bool.""" flag = self._evaluate_option(key, default=True, option=option) return bool(flag) if flag is not None else True def _evaluate_option(self, key: str, default: Any, option: Dict[str, Any]) -> Any: """Evaluates an option; if is missing, then function returns 'default', and if it is callable this calls it, passing the option key, generator's args and kwargs. The callable may take fewer arguments; if so, this will pass as many argments as it will take, even if that is none at all.""" if key not in option: return default value = option[key] return self.evaluate_option_value(value, option=option) def evaluate_option_value(self, value: Any, option: Dict[str, Any]) -> Any: """Evaluates the 'value' given, if it is callable. If not, this method just returns the 'value'. The 'value' is called with the option-key and then all the callback arguments given to this generator's __init__. If the 'value' takes fewer arguments than this, trailing arguments are omitted.""" if callable(value): sig = signature(value) argcount = len(sig.parameters) option_key = option["option"] argsneeded = 1 + len(self.callback_args) if argcount >= argsneeded: # enough declared args? return value(option_key, *self.callback_args, **self.callback_kwargs) elif any(p.kind == Parameter.VAR_POSITIONAL for p in sig.parameters.values()): # unlimited args via *args? return value(option_key, *self.callback_args, **self.callback_kwargs) elif argcount == 0: # no args? return value(**self.callback_kwargs) else: # any other number of args args = list(self.callback_args) args.insert(0, option_key) args = args[:argcount] return value(*args, **self.callback_kwargs) return value class SectionFrame(Gtk.Frame): """A frame that is styled to have particular margins, and can have its frame hidden. This leaves the content but removes the margins and borders and all that, so it looks like the frame was never there.""" def __init__(self, section, **kwargs): super().__init__(label=section, **kwargs) self.section = section self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, visible=True) self.add(self.vbox) self.get_style_context().add_class("section-frame") def has_visible_children(self): return any(w for w in self.vbox.get_children() if w.get_visible()) class WidgetWarningMessageBox(Gtk.Box): """A box to display a message with an icon inside the configuration dialog.""" def __init__(self, icon_name, margin_left=18, margin_right=18, margin_bottom=6): super().__init__( spacing=6, visible=False, margin_left=margin_left, margin_right=margin_right, margin_bottom=margin_bottom, no_show_all=True, ) image = Gtk.Image(visible=True) image.set_from_icon_name(icon_name, Gtk.IconSize.DND) self.pack_start(image, False, False, 0) self.label = Gtk.Label(visible=True, xalign=0) self.label.set_line_wrap(True) self.pack_start(self.label, False, False, 0) def show_markup(self, markup) -> bool: """Displays the markup given, and shows this box. If markup is empty or None, this hides the box instead. Returns the new visibility.""" visible = bool(markup) if markup: self.label.set_markup(str(markup)) self.set_visible(visible) return visible class ConfigMessageBox(WidgetWarningMessageBox): def __init__(self, message, icon_name, **kwargs): self.message = message super().__init__(icon_name, **kwargs) if not callable(message): text = gtk_safe(message) if text: self.label.set_markup(str(text)) def update_message(self, option: Dict[str, Any], generator: WidgetGenerator) -> bool: try: text = generator.evaluate_option_value(self.message, option) except Exception as err: logger.exception("Unable to generate configuration warning: %s", err) text = gtk_safe(str(err)) return self.show_markup(text) class ConfigWarningBox(ConfigMessageBox): def __init__(self, warning): super().__init__(warning, icon_name="dialog-warning") class ConfigErrorBox(ConfigMessageBox): def __init__(self, error): super().__init__(error, icon_name="dialog-error") @property def blocks_sensitivity(self): """Called to check if the wrapper should be made insensitive because of this error box.""" return self.get_visible()