diff --git a/lutris/game.py b/lutris/game.py index 83e7e5e18..b7c292b1a 100644 --- a/lutris/game.py +++ b/lutris/game.py @@ -18,7 +18,7 @@ from lutris.config import LutrisConfig from lutris.database import categories as categories_db from lutris.database import games as games_db from lutris.database import sql -from lutris.exceptions import GameConfigError, UnavailableRunnerError, watch_game_errors +from lutris.exceptions import GameConfigError, watch_game_errors from lutris.runner_interpreter import export_bash_script, get_launch_parameters from lutris.runners import InvalidRunner, import_runner from lutris.util import audio, discord, extract, jobs, linux, strings, system, xdgshortcuts @@ -67,38 +67,6 @@ class Game(GObject.Object): "game-installed": (GObject.SIGNAL_RUN_FIRST, None, ()), } - class LaunchUIDelegate: - """These objects provide UI for the game while it is being launched; - one provided to the launch() method. - - The default implementation provides no UI and makes default choices for - the user, but DialogLaunchUIDelegate implements this to show dialogs and ask the - user questions. Windows then inherit from DialogLaunchUIDelegate. - - If these methods throw any errors are reported via tha game-error signal; - that is not part of this delegate because errors can be report outside of - the launch() method, where no delegate is available. - """ - - def check_game_launchable(self, game): - """See if the game can be launched. If there are adverse conditions, - this can warn the user and ask whether to launch. If this returs - False, the launch is cancelled. The default is to return True with no - actual checks. - """ - if not game.runner.is_installed(): - raise UnavailableRunnerError("The required runner '%s' is not installed." % game.runner.name) - return True - - def select_game_launch_config(self, game): - """Prompt the user for which launch config to use. Returns None - if the user cancelled, an empty dict for the primary game configuration - and the launch_config as a dict if one is selected. - - The default is the select the primary game. - """ - return {} # primary game - def __init__(self, game_id=None): super().__init__() self._id = game_id # pylint: disable=invalid-name @@ -126,6 +94,7 @@ class Game(GObject.Object): self.service = game_data.get("service") self.appid = game_data.get("service_id") self.playtime = float(game_data.get("playtime") or 0.0) + self.discord_id = game_data.get('discord_id') # Discord App ID for RPC self._config = None self._runner = None @@ -146,9 +115,6 @@ class Game(GObject.Object): self.timer = Timer() self.screen_saver_inhibitor_cookie = None - # Adding Discord App ID for RPC - self.discord_id = game_data.get('discord_id') - @staticmethod def create_empty_service_game(db_game, service): """Creates a Game from the database data from ServiceGameCollection, which is diff --git a/lutris/gui/application.py b/lutris/gui/application.py index 7f68a9ca3..7b0aa9ccf 100644 --- a/lutris/gui/application.py +++ b/lutris/gui/application.py @@ -41,6 +41,7 @@ from lutris.game import Game, export_game, import_game from lutris.installer import get_installers from lutris.gui.dialogs import ErrorDialog, InstallOrPlayDialog, NoticeDialog, LutrisInitDialog from lutris.gui.dialogs.issue import IssueReportWindow +from lutris.gui.dialogs.delegates import LaunchUIDelegate, InstallUIDelegate, CommandLineUIDelegate from lutris.gui.installerwindow import InstallerWindow, InstallationKind from lutris.gui.widgets.status_icon import LutrisStatusIcon from lutris.migrations import migrate @@ -53,7 +54,6 @@ from lutris.util.steam.appmanifest import AppManifest, get_appmanifests from lutris.util.steam.config import get_steamapps_paths from lutris.services import get_enabled_services from lutris.database.services import ServiceGameCollection -from lutris.runners.runner import Runner from .lutriswindow import LutrisWindow @@ -78,8 +78,8 @@ class Application(Gtk.Application): self.force_updates = False self.css_provider = Gtk.CssProvider.new() self.window = None - self.launch_ui_delegate = Game.LaunchUIDelegate() - self.install_ui_delegate = Runner.InstallUIDelegate() + self.launch_ui_delegate = LaunchUIDelegate() + self.install_ui_delegate = InstallUIDelegate() self.running_games = Gio.ListStore.new(Game) self.app_windows = {} @@ -974,23 +974,3 @@ Also, check that the version specified is in the correct format. def has_tray_icon(self): return self.tray and self.tray.is_visible() - - -class CommandLineUIDelegate(Game.LaunchUIDelegate): - """This delegate can provide user selections that were provided on the command line.""" - - def __init__(self, launch_config_name): - self.launch_config_name = launch_config_name - - def select_game_launch_config(self, game): - if not self.launch_config_name: - return {} - - game_config = game.config.game_level.get("game", {}) - configs = game_config.get("launch_configs") - - for config in configs: - if config.get("name") == self.launch_config_name: - return config - - raise RuntimeError("The launch configuration '%s' could not be found." % self.launch_config_name) diff --git a/lutris/gui/dialogs/delegates.py b/lutris/gui/dialogs/delegates.py index c855308ea..3cb29f0df 100644 --- a/lutris/gui/dialogs/delegates.py +++ b/lutris/gui/dialogs/delegates.py @@ -2,15 +2,109 @@ from gettext import gettext as _ from gi.repository import Gdk, Gtk +from lutris.exceptions import UnavailableRunnerError from lutris.game import Game from lutris.gui import dialogs from lutris.gui.dialogs.download import DownloadDialog from lutris.runners import wine -from lutris.runners.runner import Runner from lutris.util.downloader import Downloader -class DialogInstallUIDelegate(Runner.InstallUIDelegate): +class LaunchUIDelegate: + """These objects provide UI for the game while it is being launched; + one provided to the launch() method. + + The default implementation provides no UI and makes default choices for + the user, but DialogLaunchUIDelegate implements this to show dialogs and ask the + user questions. Windows then inherit from DialogLaunchUIDelegate. + + If these methods throw any errors are reported via tha game-error signal; + that is not part of this delegate because errors can be report outside of + the launch() method, where no delegate is available. + """ + + def check_game_launchable(self, game): + """See if the game can be launched. If there are adverse conditions, + this can warn the user and ask whether to launch. If this returs + False, the launch is cancelled. The default is to return True with no + actual checks. + """ + if not game.runner.is_installed(): + raise UnavailableRunnerError("The required runner '%s' is not installed." % game.runner.name) + return True + + def select_game_launch_config(self, game): + """Prompt the user for which launch config to use. Returns None + if the user cancelled, an empty dict for the primary game configuration + and the launch_config as a dict if one is selected. + + The default is the select the primary game. + """ + return {} # primary game + + +class InstallUIDelegate: + """These objects provide UI for a runner as it is installing itself. + One of these must be provided to the install() method. + + The default implementation provides no UI and makes default choices for + the user, but DialogInstallUIDelegate implements this to show dialogs and + ask the user questions. Windows then inherit from DialogLaunchUIDelegate. + """ + + def show_install_yesno_inquiry(self, question, title): + """Called to ask the user a yes/no question. + + The default is 'yes'.""" + return True + + def show_install_file_inquiry(self, question, title, message): + """Called to ask the user for a file. + + Lutris first asks the user the question given (showing the title); + if the user answers 'Yes', it asks for the file using the message. + + Returns None if the user answers 'No' or cancels out. Returns the + file path if the user selected one. + + The default is to return None always. + """ + return None + + def download_install_file(self, url, destination): + """Downloads a file from a URL to a destination, overwriting any + file at that path. + + Returns True if sucessful, and False if the user cancels. + + The default is to download with no UI, and no option to cancel. + """ + downloader = Downloader(url, destination, overwrite=True) + downloader.start() + return downloader.join() + + +class CommandLineUIDelegate(LaunchUIDelegate): + """This delegate can provide user selections that were provided on the command line.""" + + def __init__(self, launch_config_name): + self.launch_config_name = launch_config_name + + def select_game_launch_config(self, game): + if not self.launch_config_name: + return {} + + game_config = game.config.game_level.get("game", {}) + configs = game_config.get("launch_configs") + + for config in configs: + if config.get("name") == self.launch_config_name: + return config + + raise RuntimeError("The launch configuration '%s' could not be found." % self.launch_config_name) + + +class DialogInstallUIDelegate(InstallUIDelegate): """This provides UI for runner installation via dialogs.""" def show_install_yesno_inquiry(self, question, title): @@ -37,7 +131,7 @@ class DialogInstallUIDelegate(Runner.InstallUIDelegate): return dialog.downloader.state == Downloader.COMPLETED -class DialogLaunchUIDelegate(Game.LaunchUIDelegate): +class DialogLaunchUIDelegate(LaunchUIDelegate): """This provides UI for game launch via dialogs.""" def check_game_launchable(self, game): diff --git a/lutris/runners/runner.py b/lutris/runners/runner.py index 3cbbbd771..5ea09bf08 100644 --- a/lutris/runners/runner.py +++ b/lutris/runners/runner.py @@ -10,7 +10,6 @@ from lutris.database.games import get_game_by_field from lutris.exceptions import GameConfigError, UnavailableLibrariesError from lutris.runners import RunnerInstallationError from lutris.util import flatpak, strings, system -from lutris.util.downloader import Downloader from lutris.util.extract import ExtractFailure, extract_archive from lutris.util.http import HTTPError, Request from lutris.util.linux import LINUX_SYSTEM @@ -35,46 +34,6 @@ class Runner: # pylint: disable=too-many-public-methods arch = None # If the runner is only available for an architecture that isn't x86_64 flatpak_id = None - class InstallUIDelegate: - """These objects provide UI for a runner as it is installing itself. - One of these must be provided to the install() method. - - The default implementation provides no UI and makes default choices for - the user, but DialogInstallUIDelegate implements this to show dialogs and - ask the user questions. Windows then inherit from DialogLaunchUIDelegate. - """ - - def show_install_yesno_inquiry(self, question, title): - """Called to ask the user a yes/no question. - - The default is 'yes'.""" - return True - - def show_install_file_inquiry(self, question, title, message): - """Called to ask the user for a file. - - Lutris first asks the user the question given (showing the title); - if the user answers 'Yes', it asks for the file using the message. - - Returns None if the user answers 'No' or cancels out. Returns the - file path if the user selected one. - - The default is to return None always. - """ - return None - - def download_install_file(self, url, destination): - """Downloads a file from a URL to a destination, overwriting any - file at that path. - - Returns True if sucessful, and False if the user cancels. - - The default is to download with no UI, and no option to cancel. - """ - downloader = Downloader(url, destination, overwrite=True) - downloader.start() - return downloader.join() - def __init__(self, config=None): """Initialize runner.""" if config: @@ -222,6 +181,7 @@ class Runner: # pylint: disable=too-many-public-methods return [exe] if flatpak.is_app_installed(self.flatpak_id): return flatpak.get_run_command(self.flatpak_id) + return [] def get_env(self, os_env=False, disable_runtime=False): """Return environment variables used for a game.""" diff --git a/tests/runners/test_pcsx2.py b/tests/runners/test_pcsx2.py index 5b4b52fb0..bd5234b01 100644 --- a/tests/runners/test_pcsx2.py +++ b/tests/runners/test_pcsx2.py @@ -50,7 +50,7 @@ class TestPCSX2Runner(unittest.TestCase): mock_config.game_config = {'main_file': main_file} mock_config.runner_config = {'nogui': True} self.runner.config = mock_config - expected = {'command': [self.runner.get_executable(), '-nogui', main_file]} + expected = {'command': self.runner.get_command() + ['-nogui', main_file]} self.assertEqual(self.runner.play(), expected) @patch('lutris.util.system.path_exists')