def __init__(self, core: LegendaryCore): super(GameListUninstalled, self).__init__() self.core = core self.widget = QWidget() self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.layout = QVBoxLayout() self.filter = QLineEdit() self.filter.textChanged.connect(self.filter_games) self.filter.setPlaceholderText(self.tr("Filter Games")) self.import_button = QPushButton( self.tr("Import installed Game from Epic Games Store")) self.import_button.clicked.connect(self.import_game) self.layout.addWidget(self.filter) self.layout.addWidget(self.import_button) self.widgets_uninstalled = [] games = [] installed = [i.app_name for i in core.get_installed_list()] for game in core.get_game_list(): if not game.app_name in installed: games.append(game) games = sorted(games, key=lambda x: x.app_title) for game in games: game_widget = UninstalledGameWidget(game) game_widget.finished.connect(lambda: self.reload.emit()) self.layout.addWidget(game_widget) self.widgets_uninstalled.append(game_widget) self.layout.addStretch(1) self.widget.setLayout(self.layout) self.setWidget(self.widget)
def __init__(self, parent, core: LegendaryCore): super(GameListUninstalled, self).__init__(parent=parent) self.widget = QWidget() self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.layout = QVBoxLayout() self.filter = QLineEdit() self.filter.textChanged.connect(self.filter_games) self.filter.setPlaceholderText("Filter Games") self.layout.addWidget(self.filter) self.widgets_uninstalled = [] games = [] installed = [i.app_name for i in core.get_installed_list()] for game in core.get_game_list(): if not game.app_name in installed: games.append(game) games = sorted(games, key=lambda x: x.app_title) for game in games: game_widget = UninstalledGameWidget(game) self.layout.addWidget(game_widget) self.widgets_uninstalled.append(game_widget) self.layout.addStretch(1) self.widget.setLayout(self.layout) self.setWidget(self.widget)
def launch_game(app_name: str, lgd_core: LegendaryCore, offline: bool = False, skip_version_check: bool = False, username_override=None, wine_bin: str = None, wine_prfix: str = None, language: str = None, wrapper=None, no_wine: bool = os.name == "nt", extra: [] = None): game = lgd_core.get_installed_game(app_name) if not game: print("Game not found") return None if game.is_dlc: print("Game is dlc") return None if not os.path.exists(game.install_path): print("Game doesn't exist") return None if not offline: print("logging in") if not lgd_core.login(): return None if not skip_version_check and not core.is_noupdate_game(app_name): # check updates try: latest = lgd_core.get_asset(app_name, update=True) except ValueError: print("Metadata doesn't exist") return None if latest.build_version != game.version: print("Please update game") return None params, cwd, env = lgd_core.get_launch_parameters(app_name=app_name, offline=offline, extra_args=extra, user=username_override, wine_bin=wine_bin, wine_pfx=wine_prfix, language=language, wrapper=wrapper, disable_wine=no_wine) process = QProcess() process.setWorkingDirectory(cwd) environment = QProcessEnvironment() for e in env: environment.insert(e, env[e]) process.setProcessEnvironment(environment) process.start(params[0], params[1:]) return process
def __init__(self, parent, core: LegendaryCore): super(GameListInstalled, self).__init__(parent=parent) self.widget = QWidget() self.core = core self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.layout = QVBoxLayout() self.widgets = {} for i in sorted(core.get_installed_list(), key=lambda game: game.title): widget = GameWidget(i, core) widget.signal.connect(self.remove_game) self.widgets[i.app_name] = widget self.layout.addWidget(widget) self.widget.setLayout(self.layout) self.setWidget(self.widget)
def __init__(self, core: LegendaryCore): super(LaunchDialog, self).__init__() self.title = QLabel("<h3>" + self.tr("Launching Rare") + "</h3>") self.thread = LaunchThread(core, self) self.thread.download_progess.connect(self.update_pb) self.thread.action.connect(self.info) self.info_pb = QProgressBar() self.info_pb.setMaximum(len(core.get_game_list())) self.info_text = QLabel(self.tr("Logging in")) self.layout = QVBoxLayout() self.layout.addWidget(self.title) self.layout.addWidget(self.info_pb) self.layout.addWidget(self.info_text) self.setLayout(self.layout) self.thread.start()
def __init__(self): self.core = LegendaryCore() self.logger = logging.getLogger('cli') self.logging_queue = None
class LegendaryCLI: def __init__(self): self.core = LegendaryCore() self.logger = logging.getLogger('cli') self.logging_queue = None def setup_threaded_logging(self): self.logging_queue = MPQueue(-1) shandler = logging.StreamHandler() sformatter = logging.Formatter( '[%(asctime)s] [%(name)s] %(levelname)s: %(message)s') shandler.setFormatter(sformatter) ql = QueueListener(self.logging_queue, shandler) ql.start() return ql def auth(self, args): try: logger.info('Testing existing login data if present...') if self.core.login(): logger.info( 'Stored credentials are still valid, if you wish to switch to a different' 'account, delete ~/.config/legendary/user.json and try again.' ) exit(0) except ValueError: pass except InvalidCredentialsError: logger.error( 'Stored credentials were found but were no longer valid. Continuing with login...' ) self.core.lgd.invalidate_userdata() if os.name == 'nt' and args.import_egs_auth: logger.info('Importing login session from the Epic Launcher...') try: if self.core.auth_import(): logger.info( 'Successfully imported login session from EGS!') logger.info( f'Now logged in as user "{self.core.lgd.userdata["displayName"]}"' ) exit(0) else: logger.warning( 'Login session from EGS seems to no longer be valid.') exit(1) except ValueError: logger.error( 'No EGS login session found, please login normally.') exit(1) # unfortunately the captcha stuff makes a complete CLI login flow kinda impossible right now... print('Please login via the epic web login!') webbrowser.open( 'https://www.epicgames.com/id/login?redirectUrl=https%3A%2F%2Fwww.epicgames.com%2Fid%2Fapi%2Fexchange' ) print('If web page did not open automatically, please navigate ' 'to https://www.epicgames.com/id/login in your web browser') print( '- In case you opened the link manually; please open https://www.epicgames.com/id/api/exchange ' 'in your web browser after you have finished logging in.') exchange_code = input('Please enter code from JSON response: ') exchange_token = exchange_code.strip().strip('"') if self.core.auth_code(exchange_token): logger.info( f'Successfully logged in as "{self.core.lgd.userdata["displayName"]}"' ) else: logger.error('Login attempt failed, please see log for details.') def list_games(self, args): logger.info('Logging in...') if not self.core.login(): logger.error('Login failed, cannot continue!') exit(1) logger.info('Getting game list... (this may take a while)') games, dlc_list = self.core.get_game_and_dlc_list( platform_override=args.platform_override, skip_ue=not args.include_ue) print('\nAvailable games:') for game in sorted(games, key=lambda x: x.app_title): print( f' * {game.app_title} (App name: {game.app_name}, version: {game.app_version})' ) for dlc in sorted(dlc_list[game.asset_info.catalog_item_id], key=lambda d: d.app_title): print( f' + {dlc.app_title} (App name: {dlc.app_name}, version: {dlc.app_version})' ) print(f'\nTotal: {len(games)}') def list_installed(self, args): games = self.core.get_installed_list() if args.check_updates: logger.info('Logging in to check for updates...') if not self.core.login(): logger.error('Login failed! Not checking for updates.') else: self.core.get_assets(True) print('\nInstalled games:') for game in sorted(games, key=lambda x: x.title): print( f' * {game.title} (App name: {game.app_name}, version: {game.version})' ) game_asset = self.core.get_asset(game.app_name) if game_asset.build_version != game.version: print( f' -> Update available! Installed: {game.version}, Latest: {game_asset.build_version}' ) print(f'\nTotal: {len(games)}') def launch_game(self, args, extra): app_name = args.app_name if not self.core.is_installed(app_name): logger.error(f'Game {app_name} is not currently installed!') exit(1) if self.core.is_dlc(app_name): logger.error( f'{app_name} is DLC; please launch the base game instead!') exit(1) # override with config value args.offline = self.core.is_offline_game(app_name) if not args.offline: logger.info('Logging in...') if not self.core.login(): logger.error('Login failed, cannot continue!') exit(1) if not args.skip_version_check and not self.core.is_noupdate_game( app_name): logger.info('Checking for updates...') installed = self.core.lgd.get_installed_game(app_name) latest = self.core.get_asset(app_name, update=True) if latest.build_version != installed.version: logger.error( 'Game is out of date, please update or launch with update check skipping!' ) exit(1) params, cwd, env = self.core.get_launch_parameters( app_name=app_name, offline=args.offline, extra_args=extra, user=args.user_name_override) logger.info(f'Launching {app_name}...') if args.dry_run: logger.info(f'Launch parameters: {shlex.join(params)}') logger.info(f'Working directory: {cwd}') if env: logger.info('Environment overrides:', env) else: logger.debug(f'Launch parameters: {shlex.join(params)}') logger.debug(f'Working directory: {cwd}') if env: logger.debug('Environment overrides:', env) subprocess.Popen(params, cwd=cwd, env=env) def install_game(self, args): if not self.core.login(): logger.error( 'Login failed! Cannot continue with download process.') exit(1) if args.update_only: if not self.core.is_installed(args.app_name): logger.error( f'Update requested for "{args.app_name}", but app not installed!' ) exit(1) if args.platform_override: args.no_install = True game = self.core.get_game(args.app_name, update_meta=True) if not game: logger.error( f'Could not find "{args.app_name}" in list of available games,' f'did you type the name correctly?') exit(1) if game.is_dlc: logger.info('Install candidate is DLC') app_name = game.metadata['mainGameItem']['releaseInfo'][0]['appId'] base_game = self.core.get_game(app_name) # check if base_game is actually installed if not self.core.is_installed(app_name): # download mode doesn't care about whether or not something's installed if not args.no_install: logger.fatal(f'Base game "{app_name}" is not installed!') exit(1) else: base_game = None # todo use status queue to print progress from CLI dlm, analysis, igame = self.core.prepare_download( game=game, base_game=base_game, base_path=args.base_path, force=args.force, max_shm=args.shared_memory, max_workers=args.max_workers, game_folder=args.game_folder, disable_patching=args.disable_patching, override_manifest=args.override_manifest, override_old_manifest=args.override_old_manifest, override_base_url=args.override_base_url, platform_override=args.platform_override) # game is either up to date or hasn't changed, so we have nothing to do if not analysis.dl_size: logger.info( 'Download size is 0, the game is either already up to date or has not changed. Exiting...' ) exit(0) logger.info( f'Install size: {analysis.install_size / 1024 / 1024:.02f} MiB') compression = ( 1 - (analysis.dl_size / analysis.uncompressed_dl_size)) * 100 logger.info( f'Download size: {analysis.dl_size / 1024 / 1024:.02f} MiB ' f'(Compression savings: {compression:.01f}%)') logger.info( f'Reusable size: {analysis.reuse_size / 1024 / 1024:.02f} MiB (chunks) / ' f'{analysis.unchanged / 1024 / 1024:.02f} MiB (unchanged)') res = self.core.check_installation_conditions(analysis=analysis, install=igame) if res.failures: logger.fatal( 'Download cannot proceed, the following errors occured:') for msg in sorted(res.failures): logger.fatal(msg) exit(1) if res.warnings: logger.warning( 'Installation requirements check returned the following warnings:' ) for warn in sorted(res.warnings): logger.warning(warn) if not args.yes: choice = input(f'Do you wish to install "{igame.title}"? [Y/n]: ') if choice and choice.lower()[0] != 'y': print('Aborting...') exit(0) start_t = time.time() try: # set up logging stuff (should be moved somewhere else later) dlm.logging_queue = self.logging_queue dlm.proc_debug = args.dlm_debug dlm.start() dlm.join() except Exception as e: end_t = time.time() logger.info( f'Installation failed after {end_t - start_t:.02f} seconds.') logger.warning( f'The following exception occured while waiting for the donlowader to finish: {e!r}. ' f'Try restarting the process, the resume file will be used to start where it failed. ' f'If it continues to fail please open an issue on GitHub.') else: end_t = time.time() if not args.no_install: dlcs = self.core.get_dlc_for_game(game.app_name) if dlcs: print('The following DLCs are available for this game:') for dlc in dlcs: print( f' - {dlc.app_title} (App name: {dlc.app_name}, version: {dlc.app_version})' ) # todo recursively call install with modified args to install DLC automatically (after confirm) print( 'Installing DLCs works the same as the main game, just use the DLC app name instead.' ) print( '(Automatic installation of DLC is currently not supported.)' ) postinstall = self.core.install_game(igame) if postinstall: self._handle_postinstall(postinstall, igame, yes=args.yes) logger.info( f'Finished installation process in {end_t - start_t:.02f} seconds.' ) def _handle_postinstall(self, postinstall, igame, yes=False): print('This game lists the following prequisites to be installed:') print( f'- {postinstall["name"]}: {" ".join((postinstall["path"], postinstall["args"]))}' ) if os.name == 'nt': if yes: c = 'n' # we don't want to launch anything, just silent install. else: choice = input( 'Do you wish to install the prerequisites? ([y]es, [n]o, [i]gnore): ' ) c = choice.lower()[0] if c == 'i': # just set it to installed print('Marking prerequisites as installed...') self.core.prereq_installed(igame.app_name) elif c == 'y': # set to installed and launch installation print('Launching prerequisite executable..') self.core.prereq_installed(igame.app_name) req_path, req_exec = os.path.split(postinstall['path']) work_dir = os.path.join(igame.install_path, req_path) fullpath = os.path.join(work_dir, req_exec) subprocess.Popen([fullpath, postinstall['args']], cwd=work_dir) else: logger.info('Automatic installation not available on Linux.') def uninstall_game(self, args): igame = self.core.get_installed_game(args.app_name) if not igame: logger.error( f'Game {args.app_name} not installed, cannot uninstall!') exit(0) if igame.is_dlc: logger.error('Uninstalling DLC is not supported.') exit(1) if not args.yes: choice = input( f'Do you wish to uninstall "{igame.title}"? [y/N]: ') if not choice or choice.lower()[0] != 'y': print('Aborting...') exit(0) try: logger.info( f'Removing "{igame.title}" from "{igame.install_path}"...') self.core.uninstall_game(igame) # DLCs are already removed once we delete the main game, so this just removes them from the list dlcs = self.core.get_dlc_for_game(igame.app_name) for dlc in dlcs: idlc = self.core.get_installed_game(dlc.app_name) if self.core.is_installed(dlc.app_name): logger.info(f'Uninstalling DLC "{dlc.app_name}"...') self.core.uninstall_game(idlc, delete_files=False) logger.info('Game has been uninstalled.') except Exception as e: logger.warning( f'Removing game failed: {e!r}, please remove {igame.install_path} manually.' )
class LegendaryCLI: def __init__(self): self.core = LegendaryCore() self.logger = logging.getLogger('cli') self.logging_queue = None def setup_threaded_logging(self): self.logging_queue = MPQueue(-1) shandler = logging.StreamHandler() sformatter = logging.Formatter('[%(name)s] %(levelname)s: %(message)s') shandler.setFormatter(sformatter) ql = QueueListener(self.logging_queue, shandler) ql.start() return ql def install_game(self, manifest: str, game_folder: str, override_base_url: str): game = self.core.get_game('Fortnite', update_meta=True) base_game = None logger.info('Preparing download...') # todo use status queue to print progress from CLI # This has become a little ridiculous hasn't it? dlm, analysis, igame = self.core.prepare_download( game=game, base_game=base_game, base_path=None, force=None, max_shm=None, max_workers=None, game_folder=game_folder, disable_patching=None, override_manifest=manifest, override_old_manifest=None, override_base_url=override_base_url, platform_override=None, file_prefix_filter=None, file_exclude_filter=None, file_install_tag=None, dl_optimizations=None, dl_timeout=None, repair=None, repair_use_latest=None, disable_delta=None, override_delta_manifest=None) # game is either up to date or hasn't changed, so we have nothing to doc if not analysis.dl_size: logger.info( 'Download size is 0, the game is either already up to date or has not changed. Exiting...' ) if args.repair_mode and os.path.exists(repair_file): igame = self.core.get_installed_game(game.app_name) if igame.needs_verification: igame.needs_verification = False self.core.install_game(igame) logger.debug('Removing repair file.') os.remove(repair_file) exit(0) logger.info( f'Install size: {analysis.install_size / 1024 / 1024:.02f} MiB') compression = ( 1 - (analysis.dl_size / analysis.uncompressed_dl_size)) * 100 logger.info( f'Download size: {analysis.dl_size / 1024 / 1024:.02f} MiB ' f'(Compression savings: {compression:.01f}%)') logger.info( f'Reusable size: {analysis.reuse_size / 1024 / 1024:.02f} MiB (chunks) / ' f'{analysis.unchanged / 1024 / 1024:.02f} MiB (unchanged / skipped)' ) res = self.core.check_installation_conditions(analysis=analysis, install=igame, game=game, updating=False, ignore_space_req=None) if res.warnings or res.failures: logger.info( 'Installation requirements check returned the following results:' ) if res.warnings: for warn in sorted(res.warnings): logger.warning(warn) if res.failures: for msg in sorted(res.failures): logger.fatal(msg) logger.error('Installation cannot proceed, exiting.') if not input('Do you want to continue anyway (y/N): ').lower( ).startswith('y'): exit(1) logger.info( 'Downloads are resumable, you can interrupt the download with ' 'CTRL-C and resume it using the same command later on.') start_t = time.time() try: # set up logging stuff (should be moved somewhere else later) dlm.logging_queue = self.logging_queue dlm.proc_debug = None dlm.start() dlm.join() except Exception as e: end_t = time.time() logger.info( f'Installation failed after {end_t - start_t:.02f} seconds.') logger.warning( f'The following exception occured while waiting for the donlowader to finish: {e!r}. ' f'Try restarting the process, the resume file will be used to start where it failed. ' f'If it continues to fail please open an issue on GitHub.') else: end_t = time.time() logger.info( f'Finished installation process in {end_t - start_t:.02f} seconds.' )
def download_images(signal: pyqtSignal, core: LegendaryCore): if not os.path.isdir(IMAGE_DIR): os.makedirs(IMAGE_DIR) logger.info("Create Image dir") # Download Images for i, game in enumerate( sorted(core.get_game_list(), key=lambda x: x.app_title)): if not os.path.isdir(f"{IMAGE_DIR}/" + game.app_name): os.mkdir(f"{IMAGE_DIR}/" + game.app_name) if not os.path.isfile(f"{IMAGE_DIR}/{game.app_name}/image.json"): json_data = {"DieselGameBoxTall": None, "DieselGameBoxLogo": None} else: json_data = json.load( open(f"{IMAGE_DIR}/{game.app_name}/image.json", "r")) for image in game.metadata["keyImages"]: if image["type"] == "DieselGameBoxTall" or image[ "type"] == "DieselGameBoxLogo": if json_data[ image["type"]] != image["md5"] or not os.path.isfile( f"{IMAGE_DIR}/{game.app_name}/{image['type']}.png" ): # Download json_data[image["type"]] = image["md5"] # os.remove(f"{IMAGE_DIR}/{game.app_name}/{image['type']}.png") json.dump( json_data, open(f"{IMAGE_DIR}/{game.app_name}/image.json", "w")) logger.info(f"Download Image for Game: {game.app_title}") url = image["url"] with open( f"{IMAGE_DIR}/{game.app_name}/{image['type']}.png", "wb") as f: f.write(requests.get(url).content) f.close() if not os.path.isfile(f'{IMAGE_DIR}/' + game.app_name + '/UninstalledArt.png'): if os.path.isfile(f'{IMAGE_DIR}/' + game.app_name + '/DieselGameBoxTall.png'): # finalArt = Image.open(f'{IMAGE_DIR}/' + game.app_name + '/DieselGameBoxTall.png') # finalArt.save(f'{IMAGE_DIR}/{game.app_name}/FinalArt.png') # And same with the grayscale one bg = Image.open( f"{IMAGE_DIR}/{game.app_name}/DieselGameBoxTall.png") uninstalledArt = bg.convert('L') uninstalledArt.save( f'{IMAGE_DIR}/{game.app_name}/UninstalledArt.png') elif os.path.isfile( f"{IMAGE_DIR}/{game.app_name}/DieselGameBoxLogo.png"): bg: Image.Image = Image.open( f"{IMAGE_DIR}/{game.app_name}/DieselGameBoxLogo.png") bg = bg.resize((int(bg.size[1] * 3 / 4), bg.size[1])) logo = Image.open( f'{IMAGE_DIR}/{game.app_name}/DieselGameBoxLogo.png' ).convert('RGBA') wpercent = ((bg.size[0] * (3 / 4)) / float(logo.size[0])) hsize = int((float(logo.size[1]) * float(wpercent))) logo = logo.resize((int(bg.size[0] * (3 / 4)), hsize), Image.ANTIALIAS) # Calculate where the image has to be placed pasteX = int((bg.size[0] - logo.size[0]) / 2) pasteY = int((bg.size[1] - logo.size[1]) / 2) # And finally copy the background and paste in the image # finalArt = bg.copy() # finalArt.paste(logo, (pasteX, pasteY), logo) # Write out the file # finalArt.save(f'{IMAGE_DIR}/' + game.app_name + '/FinalArt.png') logoCopy = logo.copy() logoCopy.putalpha(int(256 * 3 / 4)) logo.paste(logoCopy, logo) uninstalledArt = bg.copy() uninstalledArt.paste(logo, (pasteX, pasteY), logo) uninstalledArt = uninstalledArt.convert('L') uninstalledArt.save(f'{IMAGE_DIR}/' + game.app_name + '/UninstalledArt.png') else: logger.warning( f"File {IMAGE_DIR}/{game.app_name}/DieselGameBoxTall.png dowsn't exist" ) signal.emit(i)
import logging import os import subprocess from PyQt5.QtCore import QProcess, QProcessEnvironment, QThread from legendary.core import LegendaryCore logger = logging.getLogger("LGD") core = LegendaryCore() def get_installed(): return sorted(core.get_installed_list(), key=lambda name: name.title) def get_installed_names(): return [i.app_name for i in core.get_installed_list()] def get_not_installed(): games = [] installed = get_installed_names() for game in get_games(): if not game.app_name in installed: games.append(game) return games # return (games, dlcs) def get_games_and_dlcs(): if not core.login():
def update_list(self): self.core = LegendaryCore() del self.widget self.setWidget(QWidget()) self.init_ui() self.update()
def __init__(self, game: InstalledGame, core: LegendaryCore): super(GameWidget, self).__init__() self.core = core self.game = game self.dev = core.get_game(self.game.app_name).metadata["developer"] self.title = game.title self.app_name = game.app_name self.version = game.version self.size = game.install_size self.launch_params = game.launch_parameters # self.dev = self.game_running = False self.layout = QHBoxLayout() if os.path.exists(f"{IMAGE_DIR}/{game.app_name}/FinalArt.png"): pixmap = QPixmap(f"{IMAGE_DIR}/{game.app_name}/FinalArt.png") elif os.path.exists( f"{IMAGE_DIR}/{game.app_name}/DieselGameBoxTall.png"): pixmap = QPixmap( f"{IMAGE_DIR}/{game.app_name}/DieselGameBoxTall.png") elif os.path.exists( f"{IMAGE_DIR}/{game.app_name}/DieselGameBoxLogo.png"): pixmap = QPixmap( f"{IMAGE_DIR}/{game.app_name}/DieselGameBoxLogo.png") else: logger.warning(f"No Image found: {self.game.title}") pixmap = None if pixmap: pixmap = pixmap.scaled(180, 240) self.image = QLabel() self.image.setPixmap(pixmap) self.layout.addWidget(self.image) ##Layout on the right self.childLayout = QVBoxLayout() play_icon = self.style().standardIcon(getattr(QStyle, 'SP_MediaPlay')) settings_icon = self.style().standardIcon(getattr( QStyle, 'SP_DirIcon')) self.title_widget = QLabel(f"<h1>{self.title}</h1>") self.launch_button = QPushButton(play_icon, "Launch") self.launch_button.clicked.connect(self.launch) self.wine_rating = QLabel("Wine Rating: " + self.get_rating()) self.developer_label = QLabel("Dev: " + self.dev) self.version_label = QLabel("Version: " + str(self.version)) self.size_label = QLabel( f"Installed size: {round(self.size / (1024 ** 3), 2)} GB") self.settings_button = QPushButton(settings_icon, " Settings (Icon TODO)") self.settings_button.clicked.connect(self.settings) self.childLayout.addWidget(self.title_widget) self.childLayout.addWidget(self.launch_button) self.childLayout.addWidget(self.developer_label) self.childLayout.addWidget(self.wine_rating) self.childLayout.addWidget(self.version_label) self.childLayout.addWidget(self.size_label) self.childLayout.addWidget(self.settings_button) self.childLayout.addStretch(1) self.layout.addLayout(self.childLayout) self.layout.addStretch(1) self.setLayout(self.layout)