import os
from gettext import gettext as _
from gi.repository import Gtk
from lutris.cache import get_custom_cache_path, save_custom_cache_path, validate_custom_cache_path
from lutris.config import LutrisConfig
from lutris.gui.config.base_config_box import BaseConfigBox
from lutris.gui.config.widget_generator import WidgetWarningMessageBox
from lutris.gui.widgets.common import FileChooserEntry, Label
from lutris.runners.runner import Runner
from lutris.util.jobs import AsyncCall
from lutris.util.retroarch.firmware import scan_firmware_directory
from lutris.util.strings import human_size
class StorageBox(BaseConfigBox):
def populate(self):
self.error_boxes = {
"bios_path": StoragePathMessageBox(),
"pga_cache_path": StoragePathMessageBox(),
}
self.add(self.get_section_label(_("Paths")))
path_widgets = self.get_path_widgets()
self.pack_start(self._get_framed_options_list_box(path_widgets), False, False, 0)
self.update_pga_cache_path_warning()
def get_path_widgets(self):
widgets = []
base_runner = Runner()
bios_path = base_runner.config.system_config.get("bios_path")
path_settings = [
{
"name": _("Game library"),
"setting": "game_path",
"default": os.path.expanduser("~/Games"),
"value": base_runner.default_path,
"help": _("The default folder where you install your games."),
},
{
"name": _("Installer cache"),
"setting": "pga_cache_path",
"default": "",
"value": get_custom_cache_path() or "",
"help": _(
"If provided, files downloaded during game installs will be kept there\n"
"\nOtherwise, all downloaded files are discarded."
),
},
{
"name": _("Emulator BIOS files location"),
"setting": "bios_path",
"default": "",
"value": bios_path if bios_path else "",
"help": _("The folder Lutris will search in for emulator BIOS files if needed"),
},
]
for path_setting in path_settings:
widgets.append(self.get_directory_chooser(path_setting))
return widgets
def get_directory_chooser(self, path_setting):
label = Label()
label.set_markup("%s" % path_setting["name"])
wrapper = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4, visible=True)
wrapper.set_margin_top(16)
default_path = path_setting["default"]
directory_chooser = FileChooserEntry(
title=_("Select folder"),
action=Gtk.FileChooserAction.SELECT_FOLDER,
warn_if_non_writable_parent=True,
text=path_setting["value"],
default_path=default_path,
)
directory_chooser.connect("changed", self.on_file_chooser_changed, path_setting)
wrapper.pack_start(label, False, False, 0)
wrapper.pack_start(directory_chooser, True, True, 0)
if path_setting["help"]:
help_wrapper = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4, visible=True)
help_wrapper.add(wrapper)
help_label = Label()
help_label.set_markup("%s" % path_setting["help"])
help_wrapper.add(help_label)
wrapper = help_wrapper
if path_setting["setting"] in self.error_boxes:
warning = self.error_boxes[path_setting["setting"]]
wrapper.add(warning)
wrapper.set_margin_end(16)
wrapper.set_margin_left(16)
wrapper.set_margin_bottom(16)
return wrapper
def is_bios_path_invalid(self, bios_path):
if not bios_path:
return bios_path, "" # it's fine to not have a BIOS path
MAX_BIOS_FOLDER_SIZE = 5e9
MAX_BIOS_FILES_IN_FOLDER = 5000
MAX_BIOS_FOLDER_DEPTH = 3
bios_path_size = 0
bios_path_file_count = 0
bios_path_depth = 0
for path, _dir_names, file_names in os.walk(bios_path):
for file_name in file_names:
file_path = f"{path}/{file_name}"
if os.access(file_path, os.R_OK):
bios_path_size += os.path.getsize(file_path)
bios_path_file_count += 1
bios_path_depth = path[len(bios_path) :].count(os.sep)
if bios_path_size > MAX_BIOS_FOLDER_SIZE:
return bios_path, _("Folder is too large (%s)") % human_size(bios_path_size)
if bios_path_file_count > MAX_BIOS_FILES_IN_FOLDER:
return bios_path, _("Too many files in folder")
if bios_path_depth > MAX_BIOS_FOLDER_DEPTH:
return bios_path, _("Folder is too deep")
return bios_path, ""
def bios_path_validated_cb(self, result, error):
error_box = self.error_boxes["bios_path"]
if error:
error_box.show_markup(None)
return
bios_path, error_message = result
error_box.show_markup(error_message)
if not error_message:
lutris_config = LutrisConfig()
lutris_config.raw_system_config["bios_path"] = bios_path
lutris_config.save()
AsyncCall(scan_firmware_directory, None, bios_path)
def on_file_chooser_changed(self, entry, setting):
folder_path = entry.get_text()
if setting["setting"] == "pga_cache_path":
save_custom_cache_path(folder_path)
self.update_pga_cache_path_warning()
elif setting["setting"] == "game_path":
lutris_config = LutrisConfig()
lutris_config.raw_system_config["game_path"] = folder_path
lutris_config.save()
elif setting["setting"] == "bios_path":
AsyncCall(self.is_bios_path_invalid, self.bios_path_validated_cb, folder_path)
def update_pga_cache_path_warning(self):
cache_path = get_custom_cache_path()
if cache_path:
valid, markup = validate_custom_cache_path(cache_path)
else:
valid, markup = True, None
error_box = self.error_boxes["pga_cache_path"]
error_box.show_markup(markup if markup and not valid else None)
class StoragePathMessageBox(WidgetWarningMessageBox):
def __init__(self, icon_name="dialog-error"):
super().__init__(icon_name=icon_name)