def on_browse_files(self, widget): game = Game(self.view.selected_game) path = game.get_browse_dir() if path and os.path.exists(path): Gtk.show_uri(None, "file://" + path, Gdk.CURRENT_TIME) else: dialogs.NoticeDialog("Can't open %s \nThe folder doesn't exist." % path)
def on_browse_files(self, widget): game = Game(self.view.selected_game) path = game.get_browse_dir() if path and os.path.exists(path): subprocess.Popen(['xdg-open', path]) else: dialogs.NoticeDialog( "Can't open %s \nThe folder doesn't exist." % path)
def update_platforms(): pga_games = pga.get_games(filter_installed=True) for pga_game in pga_games: if pga_game.get('platform') or not pga_game['runner']: continue game = Game(id=pga_game['id']) game.set_platform_from_runner() game.save()
def on_browse_files(self, widget): game = Game(self.view.selected_game) path = game.get_browse_dir() if path and os.path.exists(path): system.xdg_open(path) else: dialogs.NoticeDialog( "Can't open %s \nThe folder doesn't exist." % path )
def launch_game(self, widget, _data=None): """Launch a game after it's been installed.""" widget.set_sensitive(False) self.close(widget) if self.parent: self.parent.on_game_run(game_id=self.interpreter.game_id) else: game = Game(self.interpreter.game_id) game.play()
def platform(self): """Platform""" _platform = self._pga_data["platform"] if not _platform and self.installed: game_inst = Game(self._pga_data["id"]) if game_inst.platform: _platform = game_inst.platform else: logger.debug("Game %s has no platform", self) game_inst.set_platform_from_runner() _platform = game_inst.platform return _platform
def build_game_tab(self): if self.game and self.runner_name: self.game.runner_name = self.runner_name self.game_box = GameBox(self.lutris_config, self.game) game_sw = self.build_scrolled_window(self.game_box) elif self.runner_name: game = Game(None) game.runner_name = self.runner_name self.game_box = GameBox(self.lutris_config, game) game_sw = self.build_scrolled_window(self.game_box) else: game_sw = Gtk.Label(label=self.no_runner_label) self.add_notebook_tab(game_sw, "Game options")
def fill_missing_platforms(): """Sets the platform on games where it's missing. This should never happen. """ pga_games = pga.get_games(filter_installed=True) for pga_game in pga_games: if pga_game.get("platform") or not pga_game["runner"]: continue game = Game(game_id=pga_game["id"]) logger.error("Providing missing platorm for game %s", game.slug) game.set_platform_from_runner() if game.platform: game.save(metadata_only=True)
def on_apply_button_clicked(self, widget): widget.set_sensitive(False) remove_from_library_button = self.builder.get_object( 'remove_from_library_button' ) remove_from_library = remove_from_library_button.get_active() remove_contents_button = self.builder.get_object( 'remove_contents_button' ) remove_contents = remove_contents_button.get_active() game = Game(self.game_slug) game.remove(remove_from_library, remove_contents) self.callback(self.game_slug, remove_from_library) self.on_close()
def game(self): if not self._game: self._game = self.application.get_game_by_id(self.game_id) if not self._game: self._game = Game(self.game_id) self._game.connect("game-error", self.window.on_game_error) return self._game
def initialize(self, slug=None, callback=None): self.game = Game(slug) self.callback = callback self.substitute_label(self.builder.get_object('description_label'), 'game', self.game.name) self.substitute_label( self.builder.get_object('remove_from_library_button'), 'game', self.game.name ) remove_contents_button = self.builder.get_object( 'remove_contents_button' ) try: default_path = self.game.runner.default_path except AttributeError: default_path = "/" if not is_removeable(self.game.directory, excludes=[default_path]): remove_contents_button.set_sensitive(False) path = self.game.directory or 'disk' self.substitute_label(remove_contents_button, 'path', path) cancel_button = self.builder.get_object('cancel_button') cancel_button.connect('clicked', self.on_close) apply_button = self.builder.get_object('apply_button') apply_button.connect('clicked', self.on_apply_button_clicked)
def on_save(self, _button, callback=None): """Save game info and destroy widget. Return True if success.""" if not self.is_valid(): return False name = self.name_entry.get_text() # Do not modify slug if not self.slug: self.slug = slugify(name) if not self.game: self.game = Game() if self.lutris_config.game_config_id == TEMP_CONFIG: self.lutris_config.game_config_id = self.get_config_id() runner_class = runners.import_runner(self.runner_name) runner = runner_class(self.lutris_config) self.game.name = name self.game.slug = self.slug self.game.runner_name = self.runner_name self.game.config = self.lutris_config self.game.directory = runner.game_path self.game.is_installed = True if self.runner_name in ('steam', 'winesteam'): self.game.steamid = self.lutris_config.game_config['appid'] self.game.save() self.destroy() logger.debug("Saved %s", name) self.saved = True if callback: callback()
def on_save(self, _button): """Save game info and destroy widget. Return True if success.""" if not self.is_valid(): return False name = self.name_entry.get_text() # Do not modify slug if not self.slug: self.slug = slugify(name) if not self.game: self.game = Game(self.slug) self.game.config = self.lutris_config if not self.lutris_config.game_slug: self.lutris_config.game_slug = self.slug self.lutris_config.save() runner_class = runners.import_runner(self.runner_name) runner = runner_class(self.lutris_config) self.game.name = name self.game.slug = self.slug self.game.runner_name = self.runner_name self.game.directory = runner.game_path self.game.is_installed = True self.game.save() self.destroy() logger.debug("Saved %s", name) self.saved = True
def _build_game_tab(self): if self.game and self.runner_name: self.game.runner_name = self.runner_name try: self.game.runner = runners.import_runner(self.runner_name) except runners.InvalidRunner: pass self.game_box = GameBox(self.lutris_config, self.game) game_sw = self.build_scrolled_window(self.game_box) elif self.runner_name: game = Game(None) game.runner_name = self.runner_name self.game_box = GameBox(self.lutris_config, game) game_sw = self.build_scrolled_window(self.game_box) else: game_sw = Gtk.Label(label=self.no_runner_label) self._add_notebook_tab(game_sw, "Game options")
def on_game_clicked(self, *args): """Launch a game""" game_slug = self.view.selected_game if game_slug: self.running_game = Game(game_slug) if self.running_game.is_installed: self.running_game.play() else: InstallerDialog(game_slug, self)
def add_game(self, game): name = game['name'].replace('&', "&") runner = None platform = '' runner_name = game['runner'] runner_human_name = '' if runner_name: game_inst = Game(game['id']) if not game_inst.is_installed: return if runner_name in self.runner_names: runner_human_name = self.runner_names[runner_name] else: try: runner = runners.import_runner(runner_name) except runners.InvalidRunner: game['installed'] = False else: runner_human_name = runner.human_name self.runner_names[runner_name] = runner_human_name platform = game_inst.platform if not platform: game_inst.set_platform_from_runner() platform = game_inst.platform lastplayed = '' if game['lastplayed']: lastplayed = time.strftime("%c", time.localtime(game['lastplayed'])) pixbuf = get_pixbuf_for_game(game['slug'], self.icon_type, game['installed']) self.store.append(( game['id'], game['slug'], name, pixbuf, str(game['year'] or ''), runner_name, runner_human_name, platform, game['lastplayed'], lastplayed, game['installed'] ))
def on_game_clicked(self, *args): """Launch a game.""" game_slug = self.view.selected_game if game_slug: self.running_game = Game(game_slug) if self.running_game.is_installed: self.stop_button.set_sensitive(True) self.running_game.play() else: InstallerDialog(game_slug, self)
def on_game_clicked(self, *args): """Launch a game, or install it if it is not""" game_slug = self._get_current_game_slug() if not game_slug: return self.running_game = Game(game_slug) if self.running_game.is_installed: self.stop_button.set_sensitive(True) self.running_game.play() else: InstallerDialog(game_slug, self)
def test_can_add_game(self): name_entry = self.dlg.name_entry name_entry.set_text("Test game") self.dlg.runner_dropdown.set_active(1) game_box = self.get_game_box() exe_box = game_box.get_children()[0].get_children()[0] exe_label = exe_box.get_children()[0] self.assertEqual(exe_label.get_text(), "Executable") test_exe = os.path.abspath(__file__) exe_field = exe_box.get_children()[1] exe_field.set_file(Gio.File.new_for_path(test_exe)) exe_field.emit('file-set') self.assertEqual(exe_field.get_filename(), test_exe) add_button = self.get_buttons().get_children()[1] add_button.clicked() game = Game('test-game') self.assertEqual(game.name, 'Test game') game.remove(from_library=True)
def test_can_add_game(self): name_entry = self.dlg.name_entry name_entry.set_text("Test game") self.dlg.runner_dropdown.set_active_id('linux') game_box = self.get_game_box() exe_box = game_box.get_children()[0].get_children()[0] exe_label = exe_box.get_children()[0] self.assertEqual(exe_label.get_text(), "Executable") test_exe = os.path.abspath(__file__) exe_field = exe_box.get_children()[1] exe_field.entry.set_text(test_exe) self.assertEqual(exe_field.get_text(), test_exe) add_button = self.get_buttons().get_children()[1] add_button.clicked() pga_game = pga.get_game_by_field('test-game', 'slug') self.assertTrue(pga_game) game = Game(pga_game['id']) self.assertEqual(game.name, 'Test game') game.remove(from_library=True)
def on_game_run(self, _widget=None, game_id=None): """Launch a game, or install it if it is not""" if not game_id: game_id = self._get_current_game_id() if not game_id: return self.running_game = Game(game_id) if self.running_game.is_installed: self.running_game.play() else: game_slug = self.running_game.slug self.running_game = None InstallerDialog(game_slug, self)
def on_game_clicked(self, *args): """Launch a game.""" # Wait two seconds to avoid running a game twice if time.time() - self.game_launch_time < 2: return self.game_launch_time = time.time() game_slug = self.view.selected_game if game_slug: self.running_game = Game(game_slug) if self.running_game.is_installed: self.stop_button.set_sensitive(True) self.running_game.play() else: InstallerDialog(game_slug, self)
def initialize(self, game_id=None, callback=None): self.game = Game(game_id) self.callback = callback runner = self.game.runner self.substitute_label(self.builder.get_object('description_label'), 'game', self.game.name) self.substitute_label( self.builder.get_object('remove_from_library_button'), 'game', self.game.name ) remove_contents_button = self.builder.get_object( 'remove_contents_button' ) if self.game.is_installed: path = self.game.directory or '' if hasattr(runner, 'own_game_remove_method'): remove_contents_button.set_label(runner.own_game_remove_method) remove_contents_button.set_active(True) else: try: default_path = runner.default_path except AttributeError: default_path = "/" if is_removeable(path, excludes=[default_path]): remove_contents_button.set_active(True) else: remove_contents_button.set_sensitive(False) path = 'No game folder' path = reverse_expanduser(path) self.substitute_label(remove_contents_button, 'path', path) label = remove_contents_button.get_child() label.set_use_markup(True) label.set_line_wrap(True) label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) else: remove_contents_button.hide() cancel_button = self.builder.get_object('cancel_button') cancel_button.connect('clicked', self.on_close) apply_button = self.builder.get_object('apply_button') apply_button.connect('clicked', self.on_apply_button_clicked)
def initialize(self, slug=None, callback=None): self.game = Game(slug) self.callback = callback runner = self.game.runner self.substitute_label(self.builder.get_object('description_label'), 'game', self.game.name) self.substitute_label( self.builder.get_object('remove_from_library_button'), 'game', self.game.name ) remove_contents_button = self.builder.get_object( 'remove_contents_button' ) if self.game.is_installed: if hasattr(runner, 'own_game_remove_method'): remove_contents_button.set_label(runner.own_game_remove_method) else: try: default_path = runner.default_path except AttributeError: default_path = "/" try: game_path = runner.game_path except AttributeError: game_path = '/' if not is_removeable(game_path, excludes=[default_path]): remove_contents_button.set_sensitive(False) path = self.game.directory or 'disk' self.substitute_label(remove_contents_button, 'path', path) remove_contents_button.get_children()[0].set_use_markup(True) else: remove_contents_button.hide() cancel_button = self.builder.get_object('cancel_button') cancel_button.connect('clicked', self.on_close) apply_button = self.builder.get_object('apply_button') apply_button.connect('clicked', self.on_apply_button_clicked)
class UninstallGameDialog(GtkBuilderDialog): glade_file = "dialog-uninstall-game.ui" dialog_object = "uninstall-game-dialog" @staticmethod def substitute_label(widget, name, replacement): if hasattr(widget, "get_text"): get_text = widget.get_text set_text = widget.set_text elif hasattr(widget, "get_label"): get_text = widget.get_label set_text = widget.set_label else: raise TypeError("Unsupported type %s" % type(widget)) set_text(get_text().replace("{%s}" % name, replacement)) def initialize(self, game_id=None, callback=None): self.game = Game(game_id) self.callback = callback runner = self.game.runner self.substitute_label(self.builder.get_object("description_label"), "game", self.game.name) self.substitute_label( self.builder.get_object("remove_from_library_button"), "game", self.game.name, ) remove_contents_button = self.builder.get_object( "remove_contents_button") if self.game.is_installed: path = self.game.directory or "" if hasattr(runner, "own_game_remove_method"): remove_contents_button.set_label(runner.own_game_remove_method) remove_contents_button.set_active(True) else: try: default_path = runner.default_path except AttributeError: default_path = "/" if is_removeable(path, excludes=[default_path]): remove_contents_button.set_active(True) else: remove_contents_button.set_sensitive(False) path = _("No game folder") path = reverse_expanduser(path) self.substitute_label(remove_contents_button, "path", path) label = remove_contents_button.get_child() label.set_use_markup(True) label.set_line_wrap(True) label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) else: remove_contents_button.hide() cancel_button = self.builder.get_object("cancel_button") cancel_button.connect("clicked", self.on_close) apply_button = self.builder.get_object("apply_button") apply_button.connect("clicked", self.on_apply_button_clicked) def on_apply_button_clicked(self, widget): widget.set_sensitive(False) remove_from_library_button = self.builder.get_object( "remove_from_library_button") remove_from_library = remove_from_library_button.get_active() remove_contents_button = self.builder.get_object( "remove_contents_button") remove_contents = remove_contents_button.get_active() if remove_contents and not hasattr(self.game.runner, "no_game_remove_warning"): game_dir = self.game.directory.replace("&", "&") dlg = QuestionDialog({ "question": _("Are you sure you want to delete EVERYTHING under " "\n<b>%s</b>?\n (This can't be undone)") % game_dir, "title": _("CONFIRM DANGEROUS OPERATION"), }) if dlg.result != Gtk.ResponseType.YES: widget.set_sensitive(True) return remove_from_library = self.game.remove(remove_from_library, remove_contents) self.callback(self.game.id, remove_from_library) self.on_close()
def on_game_selected(self, _widget, game_id): Game(game_id).launch()
def on_image_downloaded(self, game_slug): is_installed = Game(game_slug).is_installed self.view.update_image(game_slug, is_installed)
def remove_desktop_shortcut(self, *args): game = Game(self.view.selected_game) xdg.remove_launcher(game.slug, game.id, desktop=True)
class GameActions: """Regroup a list of callbacks for a game""" def __init__(self, application=None, window=None): self.application = application or Gio.Application.get_default() self.window = window self.game_id = None self._game = None @property def game(self): if not self._game: self._game = self.application.get_game_by_id(self.game_id) if not self._game: self._game = Game(self.game_id) self._game.connect("game-error", self.window.on_game_error) return self._game @property def is_game_running(self): return bool(self.application.get_game_by_id(self.game_id)) def set_game(self, game=None, game_id=None): if game: self._game = game self.game_id = game.id else: self._game = None self.game_id = game_id def get_game_actions(self): """Return a list of game actions and their callbacks""" return [ ("play", _("Play"), self.on_game_launch), ("stop", _("Stop"), self.on_game_stop), ("show_logs", _("Show logs"), self.on_show_logs), ("install", _("Install"), self.on_install_clicked), ("add", _("Add installed game"), self.on_add_manually), ("configure", _("Configure"), self.on_edit_game_configuration), ("favorite", _("Add to favorites"), self.on_add_favorite_game), ("deletefavorite", _("Remove from favorites"), self.on_delete_favorite_game), ("execute-script", _("Execute script"), self.on_execute_script_clicked), ("browse", _("Browse files"), self.on_browse_files), ( "desktop-shortcut", _("Create desktop shortcut"), self.on_create_desktop_shortcut, ), ( "rm-desktop-shortcut", _("Delete desktop shortcut"), self.on_remove_desktop_shortcut, ), ( "menu-shortcut", _("Create application menu shortcut"), self.on_create_menu_shortcut, ), ( "rm-menu-shortcut", _("Delete application menu shortcut"), self.on_remove_menu_shortcut, ), ("install_more", _("Install another version"), self.on_install_clicked), ("remove", _("Remove"), self.on_remove_game), ("view", _("View on Lutris.net"), self.on_view_game), ("hide", _("Hide game from library"), self.on_hide_game), ("unhide", _("Unhide game from library"), self.on_unhide_game), ] def get_displayed_entries(self): """Return a dictionary of actions that should be shown for a game""" return { "add": not self.game.is_installed, "install": not self.game.is_installed, "play": self.game.is_installed and not self.is_game_running, "stop": self.is_game_running, "configure": bool(self.game.is_installed), "browse": self.game.is_installed and self.game.runner_name != "browser", "show_logs": self.game.is_installed, "favorite": not self.game.is_favorite, "deletefavorite": self.game.is_favorite, "install_more": not self.game.service and self.game.is_installed, "execute-script": bool(self.game.is_installed and self.game.runner.system_config.get("manual_command")), "desktop-shortcut": (self.game.is_installed and not xdgshortcuts.desktop_launcher_exists( self.game.slug, self.game.id)), "menu-shortcut": (self.game.is_installed and not xdgshortcuts.menu_launcher_exists( self.game.slug, self.game.id)), "rm-desktop-shortcut": bool(self.game.is_installed and xdgshortcuts.desktop_launcher_exists( self.game.slug, self.game.id)), "rm-menu-shortcut": bool(self.game.is_installed and xdgshortcuts.menu_launcher_exists( self.game.slug, self.game.id)), "remove": True, "view": True, "hide": self.game.is_installed and not self.game.is_hidden, "unhide": self.game.is_hidden, } def on_game_launch(self, *_args): """Launch a game""" self.game.launch() def get_running_game(self): ids = self.application.get_running_game_ids() for game_id in ids: if str(game_id) == str(self.game.id): return self.game logger.warning("Game %s not in %s", self.game_id, ids) def on_game_stop(self, caller): # pylint: disable=unused-argument """Stops the game""" matched_game = self.get_running_game() if not matched_game: return if not matched_game.game_thread: logger.warning( "Game %s doesn't appear to be running, not killing it", self.game_id) return try: os.kill(matched_game.game_thread.game_process.pid, signal.SIGTERM) except ProcessLookupError as ex: logger.debug("Failed to kill game process: %s", ex) def on_show_logs(self, _widget): """Display game log""" _buffer = LOG_BUFFERS.get(self.game.id) if not _buffer: logger.info("No log for game %s", self.game) return LogWindow(title=_("Log for {}").format(self.game), buffer=_buffer, application=self.application) def on_install_clicked(self, *_args): """Install a game""" # Install the currently selected game in the UI self.game.emit("game-install") def on_add_manually(self, _widget, *_args): """Callback that presents the Add game dialog""" AddGameDialog(self.window, game=self.game, runner=self.game.runner_name) def on_edit_game_configuration(self, _widget): """Edit game preferences""" EditGameConfigDialog(self.window, self.game) def on_add_favorite_game(self, _widget): """Add to favorite Games list""" self.game.add_to_favorites() def on_delete_favorite_game(self, _widget): """delete from favorites""" self.game.remove_from_favorites() def on_hide_game(self, _widget): """Add a game to the list of hidden games""" self.game.hide() def on_unhide_game(self, _widget): """Removes a game from the list of hidden games""" self.game.unhide() def on_execute_script_clicked(self, _widget): """Execute the game's associated script""" manual_command = self.game.runner.system_config.get("manual_command") if path_exists(manual_command): MonitoredCommand( [manual_command], include_processes=[os.path.basename(manual_command)], cwd=self.game.directory, ).start() logger.info("Running %s in the background", manual_command) def on_browse_files(self, _widget): """Callback to open a game folder in the file browser""" path = self.game.get_browse_dir() if not path: dialogs.NoticeDialog(_("This game has no installation directory")) elif path_exists(path): open_uri("file://%s" % path) else: dialogs.NoticeDialog( _("Can't open %s \nThe folder doesn't exist.") % path) def on_create_menu_shortcut(self, *_args): """Add the selected game to the system's Games menu.""" xdgshortcuts.create_launcher(self.game.slug, self.game.id, self.game.name, menu=True) def on_create_desktop_shortcut(self, *_args): """Create a desktop launcher for the selected game.""" xdgshortcuts.create_launcher(self.game.slug, self.game.id, self.game.name, desktop=True) def on_remove_menu_shortcut(self, *_args): """Remove an XDG menu shortcut""" xdgshortcuts.remove_launcher(self.game.slug, self.game.id, menu=True) def on_remove_desktop_shortcut(self, *_args): """Remove a .desktop shortcut""" xdgshortcuts.remove_launcher(self.game.slug, self.game.id, desktop=True) def on_view_game(self, _widget): """Callback to open a game on lutris.net""" open_uri("https://lutris.net/games/%s" % self.game.slug) def on_remove_game(self, *_args): """Callback that present the uninstall dialog to the user""" if self.game.is_installed: UninstallGameDialog(game_id=self.game.id, parent=self.window) else: RemoveGameDialog(game_id=self.game.id, parent=self.window)
class GameDialogCommon: """Mixin for config dialogs""" no_runner_label = "Select a runner in the Game Info tab" def __init__(self): self.notebook = None self.vbox = None self.name_entry = None self.runner_box = None self.timer_id = None self.game = None self.saved = None self.slug = None self.slug_entry = None self.year_entry = None self.slug_change_button = None self.runner_dropdown = None self.banner_button = None self.icon_button = None self.game_box = None self.system_box = None self.system_sw = None self.runner_name = None self.runner_index = None self.lutris_config = None self.clipboard = None self._clipboard_buffer = None @staticmethod def build_scrolled_window(widget): """Return a scrolled window for containing config widgets""" scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolled_window.add(widget) return scrolled_window def build_notebook(self): self.notebook = Gtk.Notebook() self.vbox.pack_start(self.notebook, True, True, 10) def build_tabs(self, config_level): self.timer_id = None if config_level == "game": self._build_info_tab() self._build_game_tab() if config_level in ("game", "runner"): self._build_runner_tab(config_level) if config_level == "system": self._build_prefs_tab() self._build_sysinfo_tab() self._build_system_tab(config_level) def _build_info_tab(self): info_box = VBox() if self.game: info_box.pack_start(self._get_banner_box(), False, False, 6) # Banner info_box.pack_start(self._get_name_box(), False, False, 6) # Game name if self.game: info_box.pack_start(self._get_slug_box(), False, False, 6) # Game id self.runner_box = self._get_runner_box() info_box.pack_start(self.runner_box, False, False, 6) # Runner info_box.pack_start(self._get_year_box(), False, False, 6) # Year info_sw = self.build_scrolled_window(info_box) self._add_notebook_tab(info_sw, "Game info") def _build_prefs_tab(self): prefs_box = VBox() prefs_box.pack_start(self._get_game_cache_box(), False, False, 6) cache_help_label = Gtk.Label(visible=True) cache_help_label.set_size_request(400, -1) cache_help_label.set_markup( "If provided, this location will be used by installers to cache " "downloaded files locally for future re-use. \nIf left empty, the " "installer files are discarded after the install completion.") prefs_box.pack_start(cache_help_label, False, False, 6) info_sw = self.build_scrolled_window(prefs_box) self._add_notebook_tab(info_sw, "Lutris preferences") def _build_sysinfo_tab(self): sysinfo_grid = Gtk.Grid() sysinfo_view = Gtk.TextView() sysinfo_view.set_editable(False) sysinfo_view.set_cursor_visible(False) sysinfo_str = gather_system_info_str() text_buffer = sysinfo_view.get_buffer() text_buffer.set_text(sysinfo_str) self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) self._clipboard_buffer = sysinfo_str button_copy = Gtk.Button("Copy System Info") button_copy.connect("clicked", self._copy_text) sysinfo_grid.add(sysinfo_view) sysinfo_grid.attach_next_to(button_copy, sysinfo_view, Gtk.PositionType.BOTTOM, 1, 1) info_sw = self.build_scrolled_window(sysinfo_grid) self._add_notebook_tab(info_sw, "System Information") def _copy_text(self, widget): self.clipboard.set_text(self._clipboard_buffer, -1) def _get_game_cache_box(self): box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) label = Label("Cache path") box.pack_start(label, False, False, 0) cache_path = get_cache_path() path_chooser = FileChooserEntry( title="Set the folder for the cache path", action=Gtk.FileChooserAction.SELECT_FOLDER, path=cache_path) path_chooser.entry.connect("changed", self._on_cache_path_set) box.pack_start(path_chooser, True, True, 0) return box def _on_cache_path_set(self, entry): if self.timer_id: GLib.source_remove(self.timer_id) self.timer_id = GLib.timeout_add(1000, self.save_cache_setting, entry.get_text()) def save_cache_setting(self, value): save_cache_path(value) GLib.source_remove(self.timer_id) self.timer_id = None return False def _get_name_box(self): box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) label = Label("Name") box.pack_start(label, False, False, 0) self.name_entry = Gtk.Entry() if self.game: self.name_entry.set_text(self.game.name) box.pack_start(self.name_entry, True, True, 0) return box def _get_slug_box(self): slug_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) label = Label("Identifier") slug_box.pack_start(label, False, False, 0) self.slug_entry = SlugEntry() self.slug_entry.set_text(self.game.slug) self.slug_entry.set_sensitive(False) self.slug_entry.connect("activate", self.on_slug_entry_activate) slug_box.pack_start(self.slug_entry, True, True, 0) self.slug_change_button = Gtk.Button("Change") self.slug_change_button.connect("clicked", self.on_slug_change_clicked) slug_box.pack_start(self.slug_change_button, False, False, 0) return slug_box def _get_runner_box(self): runner_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) runner_label = Label("Runner") runner_box.pack_start(runner_label, False, False, 0) self.runner_dropdown = self._get_runner_dropdown() runner_box.pack_start(self.runner_dropdown, True, True, 0) install_runners_btn = Gtk.Button("Install runners") install_runners_btn.connect("clicked", self.on_install_runners_clicked) runner_box.pack_start(install_runners_btn, True, True, 0) return runner_box def _get_banner_box(self): banner_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) label = Label("") banner_box.pack_start(label, False, False, 0) self.banner_button = Gtk.Button() self._set_image("banner") self.banner_button.connect("clicked", self.on_custom_image_select, "banner") banner_box.pack_start(self.banner_button, False, False, 0) reset_banner_button = Gtk.Button.new_from_icon_name( "edit-clear", Gtk.IconSize.MENU) reset_banner_button.set_relief(Gtk.ReliefStyle.NONE) reset_banner_button.set_tooltip_text("Remove custom banner") reset_banner_button.connect("clicked", self.on_custom_image_reset_clicked, "banner") banner_box.pack_start(reset_banner_button, False, False, 0) self.icon_button = Gtk.Button() self._set_image("icon") self.icon_button.connect("clicked", self.on_custom_image_select, "icon") banner_box.pack_start(self.icon_button, False, False, 0) reset_icon_button = Gtk.Button.new_from_icon_name( "edit-clear", Gtk.IconSize.MENU) reset_icon_button.set_relief(Gtk.ReliefStyle.NONE) reset_icon_button.set_tooltip_text("Remove custom icon") reset_icon_button.connect("clicked", self.on_custom_image_reset_clicked, "icon") banner_box.pack_start(reset_icon_button, False, False, 0) return banner_box def _get_year_box(self): box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) label = Label("Release year") box.pack_start(label, False, False, 0) self.year_entry = NumberEntry() if self.game: self.year_entry.set_text(str(self.game.year or "")) box.pack_start(self.year_entry, True, True, 0) return box def _set_image(self, image_format): image = Gtk.Image() game_slug = self.game.slug if self.game else "" image.set_from_pixbuf(get_pixbuf_for_game(game_slug, image_format)) if image_format == "banner": self.banner_button.set_image(image) else: self.icon_button.set_image(image) def _set_icon_image(self): image = Gtk.Image() game_slug = self.game.slug if self.game else "" image.set_from_pixbuf(get_pixbuf_for_game(game_slug, "banner")) self.banner_button.set_image(image) def _get_runner_dropdown(self): runner_liststore = self._get_runner_liststore() runner_dropdown = Gtk.ComboBox.new_with_model(runner_liststore) runner_dropdown.set_id_column(1) runner_index = 0 if self.runner_name: for runner in runner_liststore: if self.runner_name == str(runner[1]): break runner_index += 1 self.runner_index = runner_index runner_dropdown.set_active(self.runner_index) runner_dropdown.connect("changed", self.on_runner_changed) cell = Gtk.CellRendererText() cell.props.ellipsize = Pango.EllipsizeMode.END runner_dropdown.pack_start(cell, True) runner_dropdown.add_attribute(cell, "text", 0) return runner_dropdown @staticmethod def _get_runner_liststore(): """Build a ListStore with available runners.""" runner_liststore = Gtk.ListStore(str, str) runner_liststore.append(("Select a runner from the list", "")) for runner in runners.get_installed(): description = runner.description runner_liststore.append( ("%s (%s)" % (runner.human_name, description), runner.name)) return runner_liststore def on_slug_change_clicked(self, widget): if self.slug_entry.get_sensitive() is False: widget.set_label("Apply") self.slug_entry.set_sensitive(True) else: self.change_game_slug() def on_slug_entry_activate(self, _widget): self.change_game_slug() def change_game_slug(self): self.slug = self.slug_entry.get_text() self.slug_entry.set_sensitive(False) self.slug_change_button.set_label("Change") def on_install_runners_clicked(self, _button): """Messed up callback requiring an import in the method to avoid a circular dependency""" from lutris.gui.dialogs.runners import RunnersDialog runners_dialog = RunnersDialog() runners_dialog.connect("runner-installed", self._update_runner_dropdown) def _update_runner_dropdown(self, _widget): active_id = self.runner_dropdown.get_active_id() self.runner_dropdown.set_model(self._get_runner_liststore()) self.runner_dropdown.set_active_id(active_id) def _build_game_tab(self): if self.game and self.runner_name: self.game.runner_name = self.runner_name if not self.game.runner or self.game.runner.name != self.runner_name: try: self.game.runner = runners.import_runner( self.runner_name)() except runners.InvalidRunner: pass self.game_box = GameBox(self.lutris_config, self.game) game_sw = self.build_scrolled_window(self.game_box) elif self.runner_name: game = Game(None) game.runner_name = self.runner_name self.game_box = GameBox(self.lutris_config, game) game_sw = self.build_scrolled_window(self.game_box) else: game_sw = Gtk.Label(label=self.no_runner_label) self._add_notebook_tab(game_sw, "Game options") def _build_runner_tab(self, _config_level): if self.runner_name: self.runner_box = RunnerBox(self.lutris_config, self.game) runner_sw = self.build_scrolled_window(self.runner_box) else: runner_sw = Gtk.Label(label=self.no_runner_label) self._add_notebook_tab(runner_sw, "Runner options") def _build_system_tab(self, _config_level): if not self.lutris_config: raise RuntimeError("Lutris config not loaded yet") self.system_box = SystemBox(self.lutris_config) self.system_sw = self.build_scrolled_window(self.system_box) self._add_notebook_tab(self.system_sw, "System options") def _add_notebook_tab(self, widget, label): self.notebook.append_page(widget, Gtk.Label(label=label)) def build_action_area(self, button_callback): self.action_area.set_layout(Gtk.ButtonBoxStyle.EDGE) # Advanced settings checkbox checkbox = Gtk.CheckButton(label="Show advanced options") value = settings.read_setting("show_advanced_options") if value == "True": checkbox.set_active(value) checkbox.connect("toggled", self.on_show_advanced_options_toggled) self.action_area.pack_start(checkbox, False, False, 5) # Buttons hbox = Gtk.Box() cancel_button = Gtk.Button(label="Cancel") cancel_button.connect("clicked", self.on_cancel_clicked) hbox.pack_start(cancel_button, True, True, 10) save_button = Gtk.Button(label="Save") save_button.connect("clicked", button_callback) hbox.pack_start(save_button, True, True, 0) self.action_area.pack_start(hbox, True, True, 0) def on_show_advanced_options_toggled(self, checkbox): value = True if checkbox.get_active() else False settings.write_setting("show_advanced_options", value) self._set_advanced_options_visible(value) def _set_advanced_options_visible(self, value): """Change visibility of advanced options across all config tabs.""" widgets = self.system_box.get_children() if self.runner_name: widgets += self.runner_box.get_children() if self.game: widgets += self.game_box.get_children() for widget in widgets: if widget.get_style_context().has_class("advanced"): widget.set_visible(value) if value: widget.set_no_show_all(not value) widget.show_all() def on_runner_changed(self, widget): """Action called when runner drop down is changed.""" new_runner_index = widget.get_active() if self.runner_index and new_runner_index != self.runner_index: dlg = QuestionDialog({ "question": "Are you sure you want to change the runner for this game ? " "This will reset the full configuration for this game and " "is not reversible.", "title": "Confirm runner change", }) if dlg.result == Gtk.ResponseType.YES: self.runner_index = new_runner_index self._switch_runner(widget) else: # Revert the dropdown menu to the previously selected runner widget.set_active(self.runner_index) else: self.runner_index = new_runner_index self._switch_runner(widget) def _switch_runner(self, widget): """Rebuilds the UI on runner change""" current_page = self.notebook.get_current_page() if self.runner_index == 0: self.runner_name = None self.lutris_config = None else: self.runner_name = widget.get_model()[self.runner_index][1] self.lutris_config = LutrisConfig(runner_slug=self.runner_name, level="game") self._rebuild_tabs() self.notebook.set_current_page(current_page) def _rebuild_tabs(self): for i in range(self.notebook.get_n_pages(), 1, -1): self.notebook.remove_page(i - 1) self._build_game_tab() self._build_runner_tab("game") self._build_system_tab("game") self.show_all() def on_cancel_clicked(self, _widget=None, _event=None): """Dialog destroy callback.""" if self.game: self.game.load_config() self.destroy() def is_valid(self): if not self.runner_name: ErrorDialog("Runner not provided") return False if not self.name_entry.get_text(): ErrorDialog("Please fill in the name") return False if (self.runner_name in ("steam", "winesteam") and self.lutris_config.game_config.get("appid") is None): ErrorDialog("Steam AppId not provided") return False return True def on_save(self, _button): """Save game info and destroy widget. Return True if success.""" if not self.is_valid(): logger.warning( "Current configuration is not valid, ignoring save request") return False name = self.name_entry.get_text() if not self.slug: self.slug = slugify(name) if not self.game: self.game = Game() year = None if self.year_entry.get_text(): year = int(self.year_entry.get_text()) if not self.lutris_config.game_config_id: self.lutris_config.game_config_id = make_game_config_id(self.slug) runner_class = runners.import_runner(self.runner_name) runner = runner_class(self.lutris_config) self.game.name = name self.game.slug = self.slug self.game.year = year self.game.game_config_id = self.lutris_config.game_config_id self.game.runner = runner self.game.runner_name = self.runner_name self.game.directory = runner.game_path self.game.is_installed = True if self.runner_name in ("steam", "winesteam"): self.game.steamid = self.lutris_config.game_config["appid"] self.game.config = self.lutris_config self.game.save() self.destroy() self.saved = True def on_custom_image_select(self, _widget, image_type): dialog = Gtk.FileChooserDialog( "Please choose a custom image", self, Gtk.FileChooserAction.OPEN, ( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK, ), ) image_filter = Gtk.FileFilter() image_filter.set_name("Images") image_filter.add_pixbuf_formats() dialog.add_filter(image_filter) response = dialog.run() if response == Gtk.ResponseType.OK: image_path = dialog.get_filename() if image_type == "banner": self.game.has_custom_banner = True dest_path = resources.get_banner_path(self.game.slug) size = BANNER_SIZE file_format = "jpeg" else: self.game.has_custom_icon = True dest_path = resources.get_icon_path(self.game.slug) size = ICON_SIZE file_format = "png" pixbuf = get_pixbuf(image_path, size) pixbuf.savev(dest_path, file_format, [], []) self._set_image(image_type) if image_type == "icon": resources.update_desktop_icons() dialog.destroy() def on_custom_image_reset_clicked(self, _widget, image_type): if image_type == "banner": self.game.has_custom_banner = False dest_path = resources.get_banner_path(self.game.slug) elif image_type == "icon": self.game.has_custom_icon = False dest_path = resources.get_icon_path(self.game.slug) else: raise ValueError("Unsupported image type %s" % image_type) os.remove(dest_path) self._set_image(image_type)
def create_desktop_shortcut(self, *args): """Create a desktop launcher for the selected game.""" game = Game(self.view.selected_game) xdg.create_launcher(game.slug, game.id, game.name, desktop=True)
class UninstallGameDialog(GtkBuilderDialog): glade_file = 'dialog-uninstall-game.ui' dialog_object = 'uninstall-game-dialog' def substitute_label(self, widget, name, replacement): if hasattr(widget, 'get_text'): get_text = widget.get_text set_text = widget.set_text elif hasattr(widget, 'get_label'): get_text = widget.get_label set_text = widget.set_label else: raise TypeError("Unsupported type %s" % type(widget)) replacement = replacement.replace('&', '&') set_text(get_text().replace("{%s}" % name, replacement)) def initialize(self, slug=None, callback=None): self.game = Game(slug) self.callback = callback runner = self.game.runner self.substitute_label(self.builder.get_object('description_label'), 'game', self.game.name) self.substitute_label( self.builder.get_object('remove_from_library_button'), 'game', self.game.name ) remove_contents_button = self.builder.get_object( 'remove_contents_button' ) if self.game.is_installed: if hasattr(runner, 'own_game_remove_method'): remove_contents_button.set_label(runner.own_game_remove_method) else: try: default_path = runner.default_path except AttributeError: default_path = "/" try: game_path = runner.game_path except AttributeError: game_path = '/' if not is_removeable(game_path, excludes=[default_path]): remove_contents_button.set_sensitive(False) path = self.game.directory or 'disk' self.substitute_label(remove_contents_button, 'path', path) remove_contents_button.get_children()[0].set_use_markup(True) else: remove_contents_button.hide() cancel_button = self.builder.get_object('cancel_button') cancel_button.connect('clicked', self.on_close) apply_button = self.builder.get_object('apply_button') apply_button.connect('clicked', self.on_apply_button_clicked) def on_apply_button_clicked(self, widget): widget.set_sensitive(False) remove_from_library_button = self.builder.get_object( 'remove_from_library_button' ) remove_from_library = remove_from_library_button.get_active() remove_contents_button = self.builder.get_object( 'remove_contents_button' ) remove_contents = remove_contents_button.get_active() if remove_contents and not hasattr(self.game.runner, 'no_game_remove_warning'): game_dir = self.game.directory.replace('&', '&') dlg = QuestionDialog({ 'question': "Are you sure you want to delete EVERYTHING under " "\n<b>%s</b>?\n (This can't be undone)" % game_dir, 'title': "CONFIRM DANGEROUS OPERATION" }) if dlg.result != Gtk.ResponseType.YES: widget.set_sensitive(True) return self.game.remove(remove_from_library, remove_contents) self.callback(self.game.slug, remove_from_library) self.on_close()
def popup(self, event, game_row=None, game=None): if game_row: game_id = game_row[COL_ID] game_slug = game_row[COL_SLUG] runner_slug = game_row[COL_RUNNER] is_installed = game_row[COL_INSTALLED] elif game: game_id = game.id game_slug = game.slug runner_slug = game.runner_name is_installed = game.is_installed # Clear existing menu for item in self.get_children(): self.remove(item) # Main items self.add_menuitems(self.main_entries) # Runner specific items runner_entries = None if runner_slug: game = game or Game(game_id) try: runner = import_runner(runner_slug)(game.config) except InvalidRunner: runner_entries = None else: runner_entries = runner.context_menu_entries if runner_entries: self.append(Gtk.SeparatorMenuItem()) self.add_menuitems(runner_entries) self.show_all() # Hide some items hiding_condition = { 'add': is_installed, 'install': is_installed, 'install_more': not is_installed, 'play': not is_installed, 'configure': not is_installed, 'desktop-shortcut': (not is_installed or xdg.desktop_launcher_exists(game_slug, game_id)), 'menu-shortcut': (not is_installed or xdg.menu_launcher_exists(game_slug, game_id)), 'rm-desktop-shortcut': (not is_installed or not xdg.desktop_launcher_exists(game_slug, game_id)), 'rm-menu-shortcut': (not is_installed or not xdg.menu_launcher_exists(game_slug, game_id)), 'browse': not is_installed or runner_slug == 'browser', } for menuitem in self.get_children(): if type(menuitem) is not Gtk.ImageMenuItem: continue action = menuitem.action_id visible = not hiding_condition.get(action) menuitem.set_visible(visible) super(ContextualMenu, self).popup(None, None, None, None, event.button, event.time)
def _write_config(self): """Write the game configuration in the DB and config file.""" if self.extends: logger.info( 'This is an extension to %s, not creating a new game entry', self.extends) return configpath = make_game_config_id(self.slug) config_filename = os.path.join(settings.CONFIG_DIR, "games/%s.yml" % configpath) if self.requires: # Load the base game config required_game = pga.get_game_by_field(self.requires, field='installer_slug') base_config = LutrisConfig( runner_slug=self.runner, game_config_id=required_game['configpath']) config = base_config.game_level else: config = { 'game': {}, } self.game_id = pga.add_or_update(name=self.game_name, runner=self.runner, slug=self.game_slug, directory=self.target_path, installed=1, installer_slug=self.slug, parent_slug=self.requires, year=self.year, steamid=self.steamid, configpath=configpath, id=self.game_id) game = Game(self.game_id) game.set_platform_from_runner() game.save() logger.debug("Saved game entry %s (%d)", self.game_slug, self.game_id) # Config update if 'system' in self.script: config['system'] = self._substitute_config(self.script['system']) if self.runner in self.script and self.script[self.runner]: config[self.runner] = self._substitute_config( self.script[self.runner]) # Game options such as exe or main_file can be added at the root of the # script as a shortcut, this integrates them into the game config # properly launcher, launcher_value = self._get_game_launcher() if type(launcher_value) == list: game_files = [] for game_file in launcher_value: if game_file in self.game_files: game_files.append(self.game_files[game_file]) else: game_files.append(game_file) config['game'][launcher] = game_files elif launcher_value: if launcher_value in self.game_files: launcher_value = (self.game_files[launcher_value]) elif self.target_path and os.path.exists( os.path.join(self.target_path, launcher_value)): launcher_value = os.path.join(self.target_path, launcher_value) config['game'][launcher] = launcher_value if 'game' in self.script: config['game'].update(self.script['game']) config['game'] = self._substitute_config(config['game']) # steamless_binary64 can be used to specify 64 bit non-steam binaries if system.IS_64BIT and 'steamless_binary64' in config['game']: config['game']['steamless_binary'] = config['game'][ 'steamless_binary64'] yaml_config = yaml.safe_dump(config, default_flow_style=False) with open(config_filename, "w") as config_file: config_file.write(yaml_config)
def on_view_game(self, widget): game = Game(self.view.selected_game) self._open_browser('https://lutris.net/games/' + game.slug)
class UninstallGameDialog(Dialog): def __init__(self, game_id, parent=None): super().__init__(parent=parent) self.set_size_request(640, 128) self.game = Game(game_id) self.delete_files = False container = Gtk.VBox(visible=True) self.get_content_area().add(container) title_label = Gtk.Label(visible=True) title_label.set_line_wrap(True) title_label.set_alignment(0, 0.5) title_label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) title_label.set_markup( "<span font_desc='14'><b>Uninstall %s</b></span>" % gtk_safe(self.game.name)) container.pack_start(title_label, False, False, 4) self.folder_label = Gtk.Label(visible=True) self.folder_label.set_alignment(0, 0.5) self.delete_button = Gtk.Button(_("Uninstall"), visible=True) self.delete_button.connect("clicked", self.on_delete_clicked) if not self.game.directory: self.folder_label.set_markup("No file will be deleted") elif len(get_games(searches={"directory": self.game.directory})) > 1: self.folder_label.set_markup( "The folder %s is used by other games and will be kept." % self.game.directory) elif is_removeable(self.game.directory): self.delete_button.set_sensitive(False) self.folder_label.set_markup("<i>Calculating size…</i>") AsyncCall(get_disk_size, self.folder_size_cb, self.game.directory) else: self.folder_label.set_markup( "Content of %s are protected and will not be deleted." % reverse_expanduser(self.game.directory)) container.pack_start(self.folder_label, False, False, 4) self.confirm_delete_button = Gtk.CheckButton() self.confirm_delete_button.set_active(True) container.pack_start(self.confirm_delete_button, False, False, 4) button_box = Gtk.HBox(visible=True) button_box.set_margin_top(30) style_context = button_box.get_style_context() style_context.add_class("linked") cancel_button = Gtk.Button(_("Cancel"), visible=True) cancel_button.connect("clicked", self.on_close) button_box.add(cancel_button) button_box.add(self.delete_button) container.pack_end(button_box, False, False, 0) self.show() def folder_size_cb(self, folder_size, error): if error: logger.error(error) return self.delete_files = True self.delete_button.set_sensitive(True) self.folder_label.hide() self.confirm_delete_button.show() self.confirm_delete_button.set_label( "Delete %s (%s)" % (reverse_expanduser(self.game.directory), human_size(folder_size))) def on_close(self, _button): self.destroy() def on_delete_clicked(self, button): button.set_sensitive(False) if not self.confirm_delete_button.get_active(): self.delete_files = False if self.delete_files and not hasattr(self.game.runner, "no_game_remove_warning"): dlg = QuestionDialog({ "question": _("Please confirm.\nEverything under <b>%s</b>\n" "will be deleted.") % gtk_safe(self.game.directory), "title": _("Permanently delete files?"), }) if dlg.result != Gtk.ResponseType.YES: button.set_sensitive(True) return if self.delete_files: self.folder_label.set_markup( "Uninstalling game and deleting files...") else: self.folder_label.set_markup("Uninstalling game...") self.game.remove(self.delete_files) self.destroy()
class GameDialogCommon(object): no_runner_label = "Select a runner in the Game Info tab" @staticmethod def build_scrolled_window(widget): scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolled_window.add_with_viewport(widget) return scrolled_window def build_notebook(self): self.notebook = Gtk.Notebook() self.vbox.pack_start(self.notebook, True, True, 10) def build_tabs(self, config_level): if config_level == 'game': self._build_info_tab() self._build_game_tab() self._build_runner_tab(config_level) self._build_system_tab(config_level) def _build_info_tab(self): info_box = VBox() info_box.pack_start(self._get_name_box(), False, False, 5) # Game name if self.game: info_box.pack_start(self._get_slug_box(), False, False, 5) # Game id info_box.pack_start(self._get_banner_box(), False, False, 5) # Banner self.runner_box = self._get_runner_box() info_box.pack_start(self.runner_box, False, False, 5) # Runner info_sw = self.build_scrolled_window(info_box) self._add_notebook_tab(info_sw, "Game info") def _get_name_box(self): box = Gtk.HBox() label = Gtk.Label(label="Name") box.pack_start(label, False, False, 20) self.name_entry = Gtk.Entry() if self.game: self.name_entry.set_text(self.game.name) box.pack_start(self.name_entry, True, True, 20) return box def _get_slug_box(self): box = Gtk.HBox() label = Gtk.Label(label="Identifier") box.pack_start(label, False, False, 20) self.slug_entry = SlugEntry() self.slug_entry.set_text(self.game.slug) self.slug_entry.set_sensitive(False) self.slug_entry.connect('activate', self.on_slug_entry_activate) box.pack_start(self.slug_entry, True, True, 0) slug_change_button = Gtk.Button("Change") slug_change_button.connect('clicked', self.on_slug_change_clicked) box.pack_start(slug_change_button, False, False, 20) return box def _get_runner_box(self): runner_box = Gtk.HBox() runner_label = Gtk.Label("Runner") runner_label.set_alignment(0.5, 0.5) self.runner_dropdown = self._get_runner_dropdown() install_runners_btn = Gtk.Button(label="Install runners") install_runners_btn.connect('clicked', self.on_install_runners_clicked) install_runners_btn.set_margin_right(20) runner_box.pack_start(runner_label, False, False, 20) runner_box.pack_start(self.runner_dropdown, False, False, 20) runner_box.pack_start(install_runners_btn, False, False, 0) return runner_box def _get_banner_box(self): banner_box = Gtk.HBox() banner_label = Gtk.Label("Banner") banner_label.set_alignment(0.5, 0.5) self.banner_button = Gtk.Button() self._set_image('banner') self.banner_button.connect('clicked', self.on_custom_image_select, 'banner') reset_banner_button = Gtk.Button.new_from_icon_name( 'edit-clear', Gtk.IconSize.MENU) reset_banner_button.set_relief(Gtk.ReliefStyle.NONE) reset_banner_button.set_tooltip_text("Remove custom banner") reset_banner_button.connect('clicked', self.on_custom_image_reset_clicked, 'banner') self.icon_button = Gtk.Button() self._set_image('icon') self.icon_button.connect('clicked', self.on_custom_image_select, 'icon') reset_icon_button = Gtk.Button.new_from_icon_name( 'edit-clear', Gtk.IconSize.MENU) reset_icon_button.set_relief(Gtk.ReliefStyle.NONE) reset_icon_button.set_tooltip_text("Remove custom icon") reset_icon_button.connect('clicked', self.on_custom_image_reset_clicked, 'icon') banner_box.pack_start(banner_label, False, False, 20) banner_box.pack_start(self.banner_button, False, False, 0) banner_box.pack_start(reset_banner_button, False, False, 0) banner_box.pack_start(self.icon_button, False, False, 0) banner_box.pack_start(reset_icon_button, False, False, 0) return banner_box def _set_image(self, image_format): assert image_format in ('banner', 'icon') image = Gtk.Image() game_slug = self.game.slug if self.game else '' image.set_from_pixbuf(get_pixbuf_for_game(game_slug, image_format)) if image_format == 'banner': self.banner_button.set_image(image) else: self.icon_button.set_image(image) def _set_icon_image(self): image = Gtk.Image() game_slug = self.game.slug if self.game else '' image.set_from_pixbuf(get_pixbuf_for_game(game_slug, 'banner')) self.banner_button.set_image(image) def _get_runner_dropdown(self): runner_liststore = self._get_runner_liststore() runner_dropdown = Gtk.ComboBox.new_with_model(runner_liststore) runner_dropdown.set_id_column(1) runner_index = 0 if self.runner_name: for runner in runner_liststore: if self.runner_name == str(runner[1]): break runner_index += 1 runner_dropdown.set_active(runner_index) runner_dropdown.connect("changed", self.on_runner_changed) cell = Gtk.CellRendererText() cell.props.ellipsize = Pango.EllipsizeMode.END runner_dropdown.pack_start(cell, True) runner_dropdown.add_attribute(cell, 'text', 0) return runner_dropdown @staticmethod def _get_runner_liststore(): """Build a ListStore with available runners.""" runner_liststore = Gtk.ListStore(str, str) runner_liststore.append(("Select a runner from the list", "")) for runner in runners.get_installed(): description = runner.description runner_liststore.append( ("%s (%s)" % (runner.name, description), runner.name)) return runner_liststore def on_slug_change_clicked(self, widget): if self.slug_entry.get_sensitive() is False: self.slug_entry.set_sensitive(True) else: self.change_game_slug() def on_slug_entry_activate(self, widget): self.change_game_slug() def change_game_slug(self): self.slug = self.slug_entry.get_text() self.slug_entry.set_sensitive(False) def on_install_runners_clicked(self, _button): runners_dialog = gui.runnersdialog.RunnersDialog() runners_dialog.connect("runner-installed", self._update_runner_dropdown) def _update_runner_dropdown(self, _widget): active_id = self.runner_dropdown.get_active_id() self.runner_dropdown.set_model(self._get_runner_liststore()) self.runner_dropdown.set_active_id(active_id) def _build_game_tab(self): if self.game and self.runner_name: self.game.runner_name = self.runner_name try: self.game.runner = runners.import_runner(self.runner_name) except runners.InvalidRunner: pass self.game_box = GameBox(self.lutris_config, self.game) game_sw = self.build_scrolled_window(self.game_box) elif self.runner_name: game = Game(None) game.runner_name = self.runner_name self.game_box = GameBox(self.lutris_config, game) game_sw = self.build_scrolled_window(self.game_box) else: game_sw = Gtk.Label(label=self.no_runner_label) self._add_notebook_tab(game_sw, "Game options") def _build_runner_tab(self, config_level): if self.runner_name: self.runner_box = RunnerBox(self.lutris_config) runner_sw = self.build_scrolled_window(self.runner_box) else: runner_sw = Gtk.Label(label=self.no_runner_label) self._add_notebook_tab(runner_sw, "Runner options") def _build_system_tab(self, config_level): self.system_box = SystemBox(self.lutris_config) self.system_sw = self.build_scrolled_window(self.system_box) self._add_notebook_tab(self.system_sw, "System options") def _add_notebook_tab(self, widget, label): self.notebook.append_page(widget, Gtk.Label(label=label)) def build_action_area(self, button_callback, callback2=None): self.action_area.set_layout(Gtk.ButtonBoxStyle.EDGE) # Advanced settings checkbox checkbox = Gtk.CheckButton(label="Show advanced options") value = settings.read_setting('show_advanced_options') if value == 'True': checkbox.set_active(value) checkbox.connect("toggled", self.on_show_advanced_options_toggled) self.action_area.pack_start(checkbox, False, False, 5) # Buttons hbox = Gtk.HBox() cancel_button = Gtk.Button(label="Cancel") cancel_button.connect("clicked", self.on_cancel_clicked) hbox.pack_start(cancel_button, True, True, 10) save_button = Gtk.Button(label="Save") if callback2: save_button.connect("clicked", button_callback, callback2) else: save_button.connect("clicked", button_callback) hbox.pack_start(save_button, True, True, 0) self.action_area.pack_start(hbox, True, True, 0) def on_show_advanced_options_toggled(self, checkbox): value = True if checkbox.get_active() else False settings.write_setting('show_advanced_options', value) self._set_advanced_options_visible(value) def _set_advanced_options_visible(self, value): """Change visibility of advanced options across all config tabs.""" widgets = self.system_box.get_children() if self.runner_name: widgets += self.runner_box.get_children() if self.game: widgets += self.game_box.get_children() for widget in widgets: if widget.get_style_context().has_class('advanced'): widget.set_visible(value) if value: widget.set_no_show_all(not value) widget.show_all() def on_runner_changed(self, widget): """Action called when runner drop down is changed.""" runner_index = widget.get_active() current_page = self.notebook.get_current_page() if runner_index == 0: self.runner_name = None self.lutris_config = LutrisConfig() else: self.runner_name = widget.get_model()[runner_index][1] self.lutris_config = LutrisConfig( runner_slug=self.runner_name, game_config_id=self.game_config_id, level='game') self._rebuild_tabs() self.notebook.set_current_page(current_page) def _rebuild_tabs(self): for i in range(self.notebook.get_n_pages(), 1, -1): self.notebook.remove_page(i - 1) self._build_game_tab() self._build_runner_tab('game') self._build_system_tab('game') self.show_all() def on_cancel_clicked(self, widget=None): """Dialog destroy callback.""" self.destroy() def is_valid(self): name = self.name_entry.get_text() if not self.runner_name: ErrorDialog("Runner not provided") return False if not name: ErrorDialog("Please fill in the name") return False return True def on_save(self, _button, callback=None): """Save game info and destroy widget. Return True if success.""" if not self.is_valid(): return False name = self.name_entry.get_text() if not self.slug: self.slug = slugify(name) if not self.game: self.game = Game() if self.lutris_config.game_config_id == TEMP_CONFIG: self.lutris_config.game_config_id = self.get_config_id() runner_class = runners.import_runner(self.runner_name) runner = runner_class(self.lutris_config) self.game.name = name self.game.slug = self.slug self.game.runner_name = self.runner_name self.game.config = self.lutris_config self.game.directory = runner.game_path self.game.is_installed = True if self.runner_name in ('steam', 'winesteam'): self.game.steamid = self.lutris_config.game_config['appid'] self.game.save() self.destroy() logger.debug("Saved %s", name) self.saved = True if callback: callback() def on_custom_image_select(self, widget, image_type): dialog = Gtk.FileChooserDialog( "Please choose a custom image", self, Gtk.FileChooserAction.OPEN, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) image_filter = Gtk.FileFilter() image_filter.set_name("Images") image_filter.add_pixbuf_formats() dialog.add_filter(image_filter) response = dialog.run() if response == Gtk.ResponseType.OK: image_path = dialog.get_filename() if image_type == 'banner': self.game.has_custom_banner = True dest_path = datapath.get_banner_path(self.game.slug) size = BANNER_SIZE file_format = 'jpeg' else: self.game.has_custom_icon = True dest_path = datapath.get_icon_path(self.game.slug) size = ICON_SIZE file_format = 'png' pixbuf = get_pixbuf(image_path, None, size) pixbuf.savev(dest_path, file_format, [], []) self._set_image(image_type) dialog.destroy() def on_custom_image_reset_clicked(self, widget, image_type): if image_type == 'banner': self.game.has_custom_banner = False dest_path = datapath.get_banner_path(self.game.slug) elif image_type == 'icon': self.game.has_custom_icon = False dest_path = datapath.get_icon_path(self.game.slug) else: raise ValueError('Unsupported image type %s', image_type) os.remove(dest_path) self._set_image(image_type)
class GameDialogCommon(object): no_runner_label = "Select a runner from the list" @staticmethod def get_runner_liststore(): """Build a ListStore with available runners.""" runner_liststore = Gtk.ListStore(str, str) runner_liststore.append(("Select a runner from the list", "")) for runner_name in runners.__all__: runner_class = runners.import_runner(runner_name) runner = runner_class() if runner.is_installed(): description = runner.description runner_liststore.append( ("%s (%s)" % (runner_name, description), runner_name) ) return runner_liststore def build_entry_box(self, entry, label_text=None): box = Gtk.HBox() if label_text: label = Gtk.Label(label=label_text) box.pack_start(label, False, False, 20) box.pack_start(entry, True, True, 20) return box def get_runner_dropdown(self): runner_liststore = self.get_runner_liststore() self.runner_dropdown = Gtk.ComboBox.new_with_model(runner_liststore) runner_index = 0 if self.game: for runner in runner_liststore: if self.runner_name == str(runner[1]): break runner_index += 1 self.runner_dropdown.set_active(runner_index) self.runner_dropdown.connect("changed", self.on_runner_changed) cell = Gtk.CellRendererText() cell.props.ellipsize = Pango.EllipsizeMode.END self.runner_dropdown.pack_start(cell, True) self.runner_dropdown.add_attribute(cell, 'text', 0) return self.runner_dropdown @staticmethod def build_scrolled_window(widget): scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolled_window.add_with_viewport(widget) return scrolled_window def build_notebook(self): self.notebook = Gtk.Notebook() self.vbox.pack_start(self.notebook, True, True, 10) def add_notebook_tab(self, widget, label): self.notebook.append_page(widget, Gtk.Label(label=label)) def build_info_tab(self): info_box = VBox() self.name_entry = Gtk.Entry() if self.game: self.name_entry.set_text(self.game.name) name_box = self.build_entry_box(self.name_entry, "Name") info_box.pack_start(name_box, False, False, 5) if self.game: self.slug_entry = Gtk.Entry() self.slug_entry.set_text(self.game.slug) self.slug_entry.set_sensitive(False) slug_box = self.build_entry_box(self.slug_entry, "Identifier") info_box.pack_start(slug_box, False, False, 5) runner_box = Gtk.HBox() label = Gtk.Label("Runner") label.set_alignment(0.5, 0.5) runner_dropdown = self.get_runner_dropdown() runner_box.pack_start(label, False, False, 20) runner_box.pack_start(runner_dropdown, False, False, 20) info_box.pack_start(runner_box, False, False, 5) info_sw = self.build_scrolled_window(info_box) self.add_notebook_tab(info_sw, "Game info") def build_game_tab(self): if self.game and self.runner_name: self.game.runner_name = self.runner_name self.game_box = GameBox(self.lutris_config, self.game) game_sw = self.build_scrolled_window(self.game_box) elif self.runner_name: game = Game(None) game.runner_name = self.runner_name self.game_box = GameBox(self.lutris_config, game) game_sw = self.build_scrolled_window(self.game_box) else: game_sw = Gtk.Label(label=self.no_runner_label) self.add_notebook_tab(game_sw, "Game options") def build_runner_tab(self, config_level): if self.runner_name: self.runner_box = RunnerBox(self.lutris_config) runner_sw = self.build_scrolled_window(self.runner_box) else: runner_sw = Gtk.Label(label=self.no_runner_label) self.add_notebook_tab(runner_sw, "Runner options") def build_system_tab(self, config_level): self.system_box = SystemBox(self.lutris_config) self.system_sw = self.build_scrolled_window(self.system_box) self.add_notebook_tab(self.system_sw, "System options") def build_tabs(self, config_level): if config_level == 'game': self.build_info_tab() self.build_game_tab() self.build_runner_tab(config_level) self.build_system_tab(config_level) def rebuild_tabs(self): for i in range(self.notebook.get_n_pages(), 1, -1): self.notebook.remove_page(i - 1) self.build_game_tab() self.build_runner_tab('game') self.build_system_tab('game') self.show_all() def build_action_area(self, label, button_callback): self.action_area.set_layout(Gtk.ButtonBoxStyle.EDGE) # Advanced settings checkbox checkbox = Gtk.CheckButton(label="Show advanced options") value = settings.read_setting('show_advanced_options') if value == 'True': checkbox.set_active(value) checkbox.connect("toggled", self.on_show_advanced_options_toggled) self.action_area.pack_start(checkbox, False, False, 5) # Buttons hbox = Gtk.HBox() cancel_button = Gtk.Button(label="Cancel") cancel_button.connect("clicked", self.on_cancel_clicked) hbox.pack_start(cancel_button, True, True, 10) button = Gtk.Button(label=label) button.connect("clicked", button_callback) hbox.pack_start(button, True, True, 0) self.action_area.pack_start(hbox, True, True, 0) def set_advanced_options_visible(self, value): """Change visibility of advanced options across all config tabs.""" widgets = self.system_box.get_children() if self.runner_name: widgets += self.runner_box.get_children() if self.game: widgets += self.game_box.get_children() for widget in widgets: if widget.get_style_context().has_class('advanced'): widget.set_visible(value) if value: widget.set_no_show_all(not value) widget.show_all() def on_show_advanced_options_toggled(self, checkbox): value = True if checkbox.get_active() else False settings.write_setting('show_advanced_options', value) self.set_advanced_options_visible(value) def on_runner_changed(self, widget): """Action called when runner drop down is changed.""" runner_index = widget.get_active() current_page = self.notebook.get_current_page() if runner_index == 0: self.runner_name = None self.lutris_config = LutrisConfig() else: self.runner_name = widget.get_model()[runner_index][1] # XXX DANGER ZONE self.lutris_config = LutrisConfig(runner_slug=self.runner_name, level='game') self.rebuild_tabs() self.notebook.set_current_page(current_page) def on_cancel_clicked(self, widget=None): """Dialog destroy callback.""" self.destroy() def is_valid(self): name = self.name_entry.get_text() if not self.runner_name: ErrorDialog("Runner not provided") return False if not name: ErrorDialog("Please fill in the name") return False return True def on_save(self, _button): """Save game info and destroy widget. Return True if success.""" if not self.is_valid(): return False name = self.name_entry.get_text() # Do not modify slug if not self.slug: self.slug = slugify(name) if not self.game: self.game = Game(self.slug) self.game.config = self.lutris_config if not self.lutris_config.game_slug: self.lutris_config.game_slug = self.slug self.lutris_config.save() runner_class = runners.import_runner(self.runner_name) runner = runner_class(self.lutris_config) self.game.name = name self.game.slug = self.slug self.game.runner_name = self.runner_name self.game.directory = runner.game_path self.game.is_installed = True self.game.save() self.destroy() logger.debug("Saved %s", name) self.saved = True
class GameDialogCommon: """Mixin for config dialogs""" no_runner_label = _("Select a runner in the Game Info tab") def __init__(self): self.notebook = None self.vbox = None self.action_area = None self.name_entry = None self.runner_box = None self.timer_id = None self.game = None self.saved = None self.slug = None self.slug_entry = None self.directory_entry = None self.year_entry = None self.slug_change_button = None self.runner_dropdown = None self.banner_button = None self.icon_button = None self.game_box = None self.system_box = None self.system_sw = None self.runner_name = None self.runner_index = None self.lutris_config = None self.clipboard = None self._clipboard_buffer = None @staticmethod def build_scrolled_window(widget): """Return a scrolled window for containing config widgets""" scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolled_window.add(widget) return scrolled_window def build_notebook(self): self.notebook = Gtk.Notebook() self.vbox.pack_start(self.notebook, True, True, 10) def build_tabs(self, config_level): self.timer_id = None if config_level == "game": self._build_info_tab() self._build_game_tab() if config_level in ("game", "runner"): self._build_runner_tab(config_level) if config_level == "system": self._build_prefs_tab() self._build_sysinfo_tab() self._build_system_tab(config_level) def _build_info_tab(self): info_box = VBox() if self.game: info_box.pack_start(self._get_banner_box(), False, False, 6) # Banner info_box.pack_start(self._get_name_box(), False, False, 6) # Game name self.runner_box = self._get_runner_box() info_box.pack_start(self.runner_box, False, False, 6) # Runner info_box.pack_start(self._get_year_box(), False, False, 6) # Year if self.game: info_box.pack_start(self._get_slug_box(), False, False, 6) info_box.pack_start(self._get_directory_box(), False, False, 6) info_sw = self.build_scrolled_window(info_box) self._add_notebook_tab(info_sw, _("Game info")) def _build_prefs_tab(self): prefs_box = VBox() settings_options = { "hide_client_on_game_start": _("Minimize client when a game is launched"), "hide_text_under_icons": _("Hide text under icons"), "show_tray_icon": _("Show Tray Icon"), } for setting_key, label in settings_options.items(): prefs_box.pack_start(self._get_setting_box(setting_key, label), False, False, 6) info_sw = self.build_scrolled_window(prefs_box) self._add_notebook_tab(info_sw, _("Lutris preferences")) def _build_sysinfo_tab(self): sysinfo_box = Gtk.VBox() sysinfo_view = LogTextView() sysinfo_view.set_cursor_visible(False) sysinfo_str = gather_system_info_str() text_buffer = sysinfo_view.get_buffer() text_buffer.set_text(sysinfo_str) self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) self._clipboard_buffer = sysinfo_str button_copy = Gtk.Button(_("Copy System Info")) button_copy.connect("clicked", self._copy_text) sysinfo_box.add(sysinfo_view) sysinfo_box.add(button_copy) info_sw = self.build_scrolled_window(sysinfo_box) self._add_notebook_tab(info_sw, _("System Information")) def _copy_text(self, widget): # pylint: disable=unused-argument self.clipboard.set_text(self._clipboard_buffer, -1) def _get_setting_box(self, setting_key, label): box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) checkbox = Gtk.CheckButton(label=label) if settings.read_setting(setting_key).lower() == "true": checkbox.set_active(True) checkbox.connect("toggled", self._on_setting_change, setting_key) box.pack_start(checkbox, True, True, 0) return box def _on_setting_change(self, widget, setting_key): """Save a setting when an option is toggled""" settings.write_setting(setting_key, widget.get_active()) def _get_name_box(self): box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) label = Label(_("Name")) box.pack_start(label, False, False, 0) self.name_entry = Gtk.Entry() if self.game: self.name_entry.set_text(self.game.name) box.pack_start(self.name_entry, True, True, 0) return box def _get_slug_box(self): slug_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) label = Label(_("Identifier")) slug_box.pack_start(label, False, False, 0) self.slug_entry = SlugEntry() self.slug_entry.set_text(self.game.slug) self.slug_entry.set_sensitive(False) self.slug_entry.connect("activate", self.on_slug_entry_activate) slug_box.pack_start(self.slug_entry, True, True, 0) self.slug_change_button = Gtk.Button(_("Change")) self.slug_change_button.connect("clicked", self.on_slug_change_clicked) slug_box.pack_start(self.slug_change_button, False, False, 0) return slug_box def _get_directory_box(self): """Return widget displaying the location of the game and allowing to move it""" box = Gtk.Box(spacing=12, margin_right=12, margin_left=12, visible=True) label = Label(_("Directory")) box.pack_start(label, False, False, 0) self.directory_entry = Gtk.Entry(visible=True) self.directory_entry.set_text(self.game.directory) self.directory_entry.set_sensitive(False) box.pack_start(self.directory_entry, True, True, 0) move_button = Gtk.Button(_("Move"), visible=True) move_button.connect("clicked", self.on_move_clicked) box.pack_start(move_button, False, False, 0) return box def _get_runner_box(self): runner_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) runner_label = Label(_("Runner")) runner_box.pack_start(runner_label, False, False, 0) self.runner_dropdown = self._get_runner_dropdown() runner_box.pack_start(self.runner_dropdown, True, True, 0) install_runners_btn = Gtk.Button(_("Install runners")) install_runners_btn.connect("clicked", self.on_install_runners_clicked) runner_box.pack_start(install_runners_btn, True, True, 0) return runner_box def _get_banner_box(self): banner_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) label = Label("") banner_box.pack_start(label, False, False, 0) self.banner_button = Gtk.Button() self._set_image("banner") self.banner_button.connect("clicked", self.on_custom_image_select, "banner") banner_box.pack_start(self.banner_button, False, False, 0) reset_banner_button = Gtk.Button.new_from_icon_name( "edit-clear", Gtk.IconSize.MENU) reset_banner_button.set_relief(Gtk.ReliefStyle.NONE) reset_banner_button.set_tooltip_text(_("Remove custom banner")) reset_banner_button.connect("clicked", self.on_custom_image_reset_clicked, "banner") banner_box.pack_start(reset_banner_button, False, False, 0) self.icon_button = Gtk.Button() self._set_image("icon") self.icon_button.connect("clicked", self.on_custom_image_select, "icon") banner_box.pack_start(self.icon_button, False, False, 0) reset_icon_button = Gtk.Button.new_from_icon_name( "edit-clear", Gtk.IconSize.MENU) reset_icon_button.set_relief(Gtk.ReliefStyle.NONE) reset_icon_button.set_tooltip_text(_("Remove custom icon")) reset_icon_button.connect("clicked", self.on_custom_image_reset_clicked, "icon") banner_box.pack_start(reset_icon_button, False, False, 0) return banner_box def _get_year_box(self): box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) label = Label(_("Release year")) box.pack_start(label, False, False, 0) self.year_entry = NumberEntry() if self.game: self.year_entry.set_text(str(self.game.year or "")) box.pack_start(self.year_entry, True, True, 0) return box def _set_image(self, image_format): image = Gtk.Image() size = BANNER_SIZE if image_format == "banner" else ICON_SIZE game_slug = self.game.slug if self.game else "" image.set_from_pixbuf(get_pixbuf_for_game(game_slug, size)) if image_format == "banner": self.banner_button.set_image(image) else: self.icon_button.set_image(image) def _get_runner_dropdown(self): runner_liststore = self._get_runner_liststore() runner_dropdown = Gtk.ComboBox.new_with_model(runner_liststore) runner_dropdown.set_id_column(1) runner_index = 0 if self.runner_name: for runner in runner_liststore: if self.runner_name == str(runner[1]): break runner_index += 1 self.runner_index = runner_index runner_dropdown.set_active(self.runner_index) runner_dropdown.connect("changed", self.on_runner_changed) cell = Gtk.CellRendererText() cell.props.ellipsize = Pango.EllipsizeMode.END runner_dropdown.pack_start(cell, True) runner_dropdown.add_attribute(cell, "text", 0) return runner_dropdown @staticmethod def _get_runner_liststore(): """Build a ListStore with available runners.""" runner_liststore = Gtk.ListStore(str, str) runner_liststore.append((_("Select a runner from the list"), "")) for runner in runners.get_installed(): description = runner.description runner_liststore.append( ("%s (%s)" % (runner.human_name, description), runner.name)) return runner_liststore def on_slug_change_clicked(self, widget): if self.slug_entry.get_sensitive() is False: widget.set_label(_("Apply")) self.slug_entry.set_sensitive(True) else: self.change_game_slug() def on_slug_entry_activate(self, _widget): self.change_game_slug() def change_game_slug(self): self.slug = self.slug_entry.get_text() self.slug_entry.set_sensitive(False) self.slug_change_button.set_label(_("Change")) def on_move_clicked(self, _button): new_location = DirectoryDialog("Select new location for the game", default_path=self.game.directory) new_directory = self.game.move(new_location.folder) if new_directory: self.directory_entry.set_text(new_directory) def on_install_runners_clicked(self, _button): """Messed up callback requiring an import in the method to avoid a circular dependency""" from lutris.gui.dialogs.runners import RunnersDialog runners_dialog = RunnersDialog() runners_dialog.connect("runner-installed", self.on_runner_installed) def on_runner_installed(self, _dialog): """Callback triggered when new runners are installed""" active_id = self.runner_dropdown.get_active_id() self.runner_dropdown.set_model(self._get_runner_liststore()) self.runner_dropdown.set_active_id(active_id) def _build_game_tab(self): if self.game and self.runner_name: self.game.runner_name = self.runner_name if not self.game.runner or self.game.runner.name != self.runner_name: try: self.game.runner = runners.import_runner( self.runner_name)() except runners.InvalidRunner: pass self.game_box = GameBox(self.lutris_config, self.game) game_sw = self.build_scrolled_window(self.game_box) elif self.runner_name: game = Game(None) game.runner_name = self.runner_name self.game_box = GameBox(self.lutris_config, game) game_sw = self.build_scrolled_window(self.game_box) else: game_sw = Gtk.Label(label=self.no_runner_label) self._add_notebook_tab(game_sw, _("Game options")) def _build_runner_tab(self, _config_level): if self.runner_name: self.runner_box = RunnerBox(self.lutris_config, self.game) runner_sw = self.build_scrolled_window(self.runner_box) else: runner_sw = Gtk.Label(label=self.no_runner_label) self._add_notebook_tab(runner_sw, _("Runner options")) def _build_system_tab(self, _config_level): if not self.lutris_config: raise RuntimeError("Lutris config not loaded yet") self.system_box = SystemBox(self.lutris_config) self.system_sw = self.build_scrolled_window(self.system_box) self._add_notebook_tab(self.system_sw, _("System options")) def _add_notebook_tab(self, widget, label): self.notebook.append_page(widget, Gtk.Label(label=label)) def build_action_area(self, button_callback): self.action_area.set_layout(Gtk.ButtonBoxStyle.EDGE) # Advanced settings checkbox checkbox = Gtk.CheckButton(label=_("Show advanced options")) if settings.read_setting("show_advanced_options") == "True": checkbox.set_active(True) checkbox.connect("toggled", self.on_show_advanced_options_toggled) self.action_area.pack_start(checkbox, False, False, 5) # Buttons hbox = Gtk.Box() cancel_button = Gtk.Button(label=_("Cancel")) cancel_button.connect("clicked", self.on_cancel_clicked) hbox.pack_start(cancel_button, True, True, 10) save_button = Gtk.Button(label=_("Save")) save_button.connect("clicked", button_callback) hbox.pack_start(save_button, True, True, 0) self.action_area.pack_start(hbox, True, True, 0) def on_show_advanced_options_toggled(self, checkbox): value = bool(checkbox.get_active()) settings.write_setting("show_advanced_options", value) self._set_advanced_options_visible(value) def _set_advanced_options_visible(self, value): """Change visibility of advanced options across all config tabs.""" widgets = self.system_box.get_children() if self.runner_name: widgets += self.runner_box.get_children() if self.game: widgets += self.game_box.get_children() for widget in widgets: if widget.get_style_context().has_class("advanced"): widget.set_visible(value) if value: widget.set_no_show_all(not value) widget.show_all() def on_runner_changed(self, widget): """Action called when runner drop down is changed.""" new_runner_index = widget.get_active() if self.runner_index and new_runner_index != self.runner_index: dlg = QuestionDialog({ "question": _("Are you sure you want to change the runner for this game ? " "This will reset the full configuration for this game and " "is not reversible."), "title": _("Confirm runner change"), }) if dlg.result == Gtk.ResponseType.YES: self.runner_index = new_runner_index self._switch_runner(widget) else: # Revert the dropdown menu to the previously selected runner widget.set_active(self.runner_index) else: self.runner_index = new_runner_index self._switch_runner(widget) def _switch_runner(self, widget): """Rebuilds the UI on runner change""" current_page = self.notebook.get_current_page() if self.runner_index == 0: logger.info("No runner selected, resetting configuration") self.runner_name = None self.lutris_config = None else: runner_name = widget.get_model()[self.runner_index][1] if runner_name == self.runner_name: logger.debug("Runner unchanged, not creating a new config") return logger.info("Creating new configuration with runner %s", runner_name) self.runner_name = runner_name self.lutris_config = LutrisConfig(runner_slug=self.runner_name, level="game") self._rebuild_tabs() self.notebook.set_current_page(current_page) def _rebuild_tabs(self): for i in range(self.notebook.get_n_pages(), 1, -1): self.notebook.remove_page(i - 1) self._build_game_tab() self._build_runner_tab("game") self._build_system_tab("game") self.show_all() def on_cancel_clicked(self, _widget=None, _event=None): """Dialog destroy callback.""" if self.game: self.game.load_config() self.destroy() def is_valid(self): if not self.runner_name: ErrorDialog(_("Runner not provided")) return False if not self.name_entry.get_text(): ErrorDialog(_("Please fill in the name")) return False if (self.runner_name in ("steam", "winesteam") and self.lutris_config.game_config.get("appid") is None): ErrorDialog(_("Steam AppId not provided")) return False invalid_fields = [] runner_class = import_runner(self.runner_name) runner_instance = runner_class() for config in ["game", "runner"]: for k, v in getattr(self.lutris_config, config + "_config").items(): option = runner_instance.find_option(config + "_options", k) if option is None: continue validator = option.get("validator") if validator is not None: try: res = validator(v) logger.debug("%s validated successfully: %s", k, res) except Exception: invalid_fields.append(option.get("label")) if invalid_fields: ErrorDialog( _("The following fields have invalid values: ") + ", ".join(invalid_fields)) return False return True def on_save(self, _button): """Save game info and destroy widget. Return True if success.""" if not self.is_valid(): logger.warning( _("Current configuration is not valid, ignoring save request")) return False name = self.name_entry.get_text() if not self.slug: self.slug = slugify(name) if not self.game: self.game = Game() year = None if self.year_entry.get_text(): year = int(self.year_entry.get_text()) if not self.lutris_config.game_config_id: self.lutris_config.game_config_id = make_game_config_id(self.slug) runner_class = runners.import_runner(self.runner_name) runner = runner_class(self.lutris_config) self.game.name = name self.game.slug = self.slug self.game.year = year self.game.game_config_id = self.lutris_config.game_config_id self.game.runner = runner self.game.runner_name = self.runner_name self.game.directory = runner.game_path self.game.is_installed = True self.game.config = self.lutris_config self.game.save(save_config=True) self.destroy() self.saved = True return True def on_custom_image_select(self, _widget, image_type): dialog = Gtk.FileChooserDialog( _("Please choose a custom image"), self, Gtk.FileChooserAction.OPEN, ( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK, ), ) image_filter = Gtk.FileFilter() image_filter.set_name(_("Images")) image_filter.add_pixbuf_formats() dialog.add_filter(image_filter) response = dialog.run() if response == Gtk.ResponseType.OK: image_path = dialog.get_filename() if image_type == "banner": self.game.has_custom_banner = True dest_path = os.path.join(settings.BANNER_PATH, "%s.jpg" % self.game.slug) size = BANNER_SIZE file_format = "jpeg" else: self.game.has_custom_icon = True dest_path = resources.get_icon_path(self.game.slug) size = ICON_SIZE file_format = "png" pixbuf = get_pixbuf(image_path, size) pixbuf.savev(dest_path, file_format, [], []) self._set_image(image_type) if image_type == "icon": system.update_desktop_icons() dialog.destroy() def on_custom_image_reset_clicked(self, _widget, image_type): if image_type == "banner": self.game.has_custom_banner = False dest_path = os.path.join(settings.BANNER_PATH, "%s.jpg" % self.game.slug) elif image_type == "icon": self.game.has_custom_icon = False dest_path = resources.get_icon_path(self.game.slug) else: raise ValueError("Unsupported image type %s" % image_type) os.remove(dest_path) self._set_image(image_type)
def on_game_selected(self, _widget, game_id): self.application.launch(Game(game_id))
class LutrisWindow(object): """Handler class for main window signals.""" def __init__(self): ui_filename = os.path.join(datapath.get(), 'ui', 'LutrisWindow.ui') if not os.path.exists(ui_filename): raise IOError('File %s not found' % ui_filename) # Currently running game self.running_game = None # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 self.game_selection_time = 0 self.game_launch_time = 0 self.last_selected_game = None self.builder = Gtk.Builder() self.builder.add_from_file(ui_filename) # load config width = int(settings.read_setting('width') or 800) height = int(settings.read_setting('height') or 600) self.window_size = (width, height) view_type = self.get_view_type() self.icon_type = self.get_icon_type(view_type) filter_installed_setting = settings.read_setting( 'filter_installed') or 'false' self.filter_installed = filter_installed_setting == 'true' show_installed_games_menuitem = self.builder.get_object( 'filter_installed') show_installed_games_menuitem.set_active(self.filter_installed) logger.debug("Getting game list") game_list = get_game_list(self.filter_installed) logger.debug("Switching view") self.view = load_view(view_type, game_list, icon_type=self.icon_type) logger.debug("Connecting signals") self.main_box = self.builder.get_object('main_box') self.splash_box = self.builder.get_object('splash_box') # View menu self.grid_view_menuitem = self.builder.get_object("gridview_menuitem") self.grid_view_menuitem.set_active(view_type == 'grid') self.list_view_menuitem = self.builder.get_object("listview_menuitem") self.list_view_menuitem.set_active(view_type == 'list') # View buttons self.grid_view_btn = self.builder.get_object('switch_grid_view_btn') self.grid_view_btn.set_active(view_type == 'grid') self.list_view_btn = self.builder.get_object('switch_list_view_btn') self.list_view_btn.set_active(view_type == 'list') # Icon type menu self.banner_small_menuitem = \ self.builder.get_object('banner_small_menuitem') self.banner_small_menuitem.set_active(self.icon_type == 'banner_small') self.banner_menuitem = self.builder.get_object('banner_menuitem') self.banner_menuitem.set_active(self.icon_type == 'banner') self.icon_menuitem = self.builder.get_object('icon_menuitem') self.icon_menuitem.set_active(self.icon_type == 'grid') self.search_entry = self.builder.get_object('search_entry') # Scroll window self.games_scrollwindow = self.builder.get_object('games_scrollwindow') self.games_scrollwindow.add(self.view) # Status bar self.status_label = self.builder.get_object('status_label') self.joystick_icons = [] # Buttons self.stop_button = self.builder.get_object('stop_button') self.stop_button.set_sensitive(False) self.delete_button = self.builder.get_object('delete_button') self.delete_button.set_sensitive(False) self.play_button = self.builder.get_object('play_button') self.play_button.set_sensitive(False) # Contextual menu menu_callbacks = [ ('play', self.on_game_clicked), ('install', self.on_game_clicked), ('add', self.add_manually), ('configure', self.edit_game_configuration), ('browse', self.on_browse_files), ('desktop-shortcut', self.create_desktop_shortcut), ('menu-shortcut', self.create_menu_shortcut), ('remove', self.on_remove_game), ] self.menu = ContextualMenu(menu_callbacks) self.view.contextual_menu = self.menu # Timer self.timer_id = GLib.timeout_add(2000, self.refresh_status) # Window initialization self.window = self.builder.get_object("window") self.window.resize_to_geometry(width, height) self.window.show_all() self.builder.connect_signals(self) self.connect_signals() # XXX Hide PGA config menu item until it actually gets implemented pga_menuitem = self.builder.get_object('pga_menuitem') pga_menuitem.hide() self.switch_splash_screen() credentials = api.read_api_key() if credentials: self.on_connect_success(None, credentials) else: self.toggle_connection(False) async_call(self.sync_icons, None) @property def current_view_type(self): return 'grid' \ if self.view.__class__.__name__ == "GameGridView" \ else 'list' def switch_splash_screen(self): if self.view.n_games == 0: self.splash_box.show() self.games_scrollwindow.hide() else: self.splash_box.hide() self.games_scrollwindow.show() def sync_icons(self): game_list = pga.get_games() resources.fetch_icons([game_info['slug'] for game_info in game_list], callback=self.on_image_downloaded) def connect_signals(self): """Connect signals from the view with the main window. This must be called each time the view is rebuilt. """ self.view.connect('game-installed', self.on_game_installed) self.view.connect("game-activated", self.on_game_clicked) self.view.connect("game-selected", self.game_selection_changed) self.window.connect("configure-event", self.get_size) def get_view_type(self): view_type = settings.read_setting('view_type') if view_type in ['grid', 'list']: return view_type return settings.GAME_VIEW def get_icon_type(self, view_type): """Return the icon style depending on the type of view.""" if view_type == 'list': icon_type = settings.read_setting('icon_type_listview') default = settings.ICON_TYPE_LISTVIEW else: icon_type = settings.read_setting('icon_type_gridview') default = settings.ICON_TYPE_GRIDVIEW if icon_type not in ("banner_small", "banner", "icon"): icon_type = default return icon_type def get_size(self, widget, _): self.window_size = widget.get_size() def refresh_status(self): """Refresh status bar.""" if self.running_game: if hasattr(self.running_game.game_thread, "pid"): pid = self.running_game.game_thread.pid name = self.running_game.name if pid == 99999: self.status_label.set_text("Preparing to launch %s" % name) elif pid is None: self.status_label.set_text("Game has quit") else: self.status_label.set_text("Playing %s (pid: %r)" % (name, pid)) for index in range(4): self.joystick_icons.append( self.builder.get_object('js' + str(index) + 'image')) if os.path.exists("/dev/input/js%d" % index): self.joystick_icons[index].set_visible(True) else: self.joystick_icons[index].set_visible(False) return True def about(self, _widget, _data=None): """Open the about dialog.""" dialogs.AboutDialog() def on_remove_game(self, _widget, _data=None): selected_game = self.view.selected_game UninstallGameDialog(slug=selected_game, callback=self.on_game_deleted) def on_game_deleted(self, game_slug, from_library=False): if from_library: self.view.remove_game(game_slug) self.switch_splash_screen() else: self.view.set_uninstalled(game_slug) # Callbacks def on_connect(self, *args): """Callback when a user connects to his account.""" login_dialog = dialogs.ClientLoginDialog() login_dialog.connect('connected', self.on_connect_success) def on_disconnect(self, *args): api.disconnect() self.toggle_connection(False) def toggle_connection(self, is_connected, username=None): disconnect_menuitem = self.builder.get_object('disconnect_menuitem') connect_menuitem = self.builder.get_object('connect_menuitem') connection_label = self.builder.get_object('connection_label') if is_connected: disconnect_menuitem.show() connect_menuitem.hide() connection_status = "Connected as %s" % username else: disconnect_menuitem.hide() connect_menuitem.show() connection_status = "Not connected" logger.info(connection_status) connection_label.set_text(connection_status) def on_connect_success(self, dialog, credentials): if isinstance(credentials, str): username = credentials else: username = credentials["username"] self.toggle_connection(True, username) self.sync_library() def on_destroy(self, *args): """Signal for window close.""" view_type = 'grid' if 'GridView' in str(type(self.view)) else 'list' settings.write_setting('view_type', view_type) width, height = self.window_size settings.write_setting('width', width) settings.write_setting('height', height) Gtk.main_quit(*args) logger.debug("Quitting lutris") def on_game_installed(self, view, slug): view.set_installed(Game(slug)) def on_runners_activate(self, _widget, _data=None): """Callback when manage runners is activated.""" RunnersDialog() def on_preferences_activate(self, _widget, _data=None): """Callback when preferences is activated.""" SystemConfigDialog() def on_show_installed_games_toggled(self, widget, data=None): self.filter_installed = widget.get_active() setting_value = 'true' if self.filter_installed else 'false' settings.write_setting('filter_installed', setting_value) self.switch_view(self.current_view_type) def on_pga_menuitem_activate(self, _widget, _data=None): dialogs.PgaSourceDialog() def on_image_downloaded(self, game_slug): is_installed = Game(game_slug).is_installed self.view.update_image(game_slug, is_installed) def on_search_entry_changed(self, widget): self.view.emit('filter-updated', widget.get_text()) def on_game_clicked(self, *args): """Launch a game.""" # Wait two seconds to avoid running a game twice if time.time() - self.game_launch_time < 2: return self.game_launch_time = time.time() game_slug = self.view.selected_game if game_slug: self.running_game = Game(game_slug) if self.running_game.is_installed: self.stop_button.set_sensitive(True) self.running_game.play() else: InstallerDialog(game_slug, self) def set_status(self, text): self.status_label.set_text(text) def sync_library(self): def set_library_synced(result, error): self.set_status("Library synced") self.switch_splash_screen() self.set_status("Syncing library") async_call( api.sync, lambda r, e: async_call(self.sync_icons, set_library_synced), caller=self) def reset(self, *args): """Reset the desktop to it's initial state.""" if self.running_game: self.running_game.quit_game() self.status_label.set_text("Stopped %s" % self.running_game.name) self.running_game = None self.stop_button.set_sensitive(False) def game_selection_changed(self, _widget): # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 if type(self.view) is GameGridView: is_double_click = time.time() - self.game_selection_time < 0.4 is_same_game = self.view.selected_game == self.last_selected_game if is_double_click and is_same_game: self.on_game_clicked() self.game_selection_time = time.time() self.last_selected_game = self.view.selected_game sensitive = True if self.view.selected_game else False self.play_button.set_sensitive(sensitive) self.delete_button.set_sensitive(sensitive) def add_game_to_view(self, slug): if not slug: raise ValueError("Missing game slug") game = Game(slug) def do_add_game(): self.view.add_game(game) self.switch_splash_screen() GLib.idle_add(do_add_game) def add_game(self, _widget, _data=None): """Add a new game.""" add_game_dialog = AddGameDialog(self) add_game_dialog.run() if add_game_dialog.runner_name and add_game_dialog.slug: self.add_game_to_view(add_game_dialog.slug) def add_manually(self, *args): game = Game(self.view.selected_game) add_game_dialog = AddGameDialog(self, game) add_game_dialog.run() if add_game_dialog.installed: self.view.set_installed(game) def on_browse_files(self, widget): game = Game(self.view.selected_game) path = game.get_browse_dir() if path and os.path.exists(path): subprocess.Popen(['xdg-open', path]) else: dialogs.NoticeDialog("Can't open %s \nThe folder doesn't exist." % path) def edit_game_configuration(self, _button): """Edit game preferences.""" game = Game(self.view.selected_game) if game.is_installed: EditGameConfigDialog(self, game) def on_viewmenu_toggled(self, menuitem): view_type = 'grid' if menuitem.get_active() else 'list' if view_type == self.current_view_type: return self.switch_view(view_type) self.grid_view_btn.set_active(view_type == 'grid') self.list_view_btn.set_active(view_type == 'list') def on_viewbtn_toggled(self, widget): view_type = 'grid' if widget.get_active() else 'list' if view_type == self.current_view_type: return self.switch_view(view_type) self.grid_view_menuitem.set_active(view_type == 'grid') self.list_view_menuitem.set_active(view_type == 'list') def switch_view(self, view_type): """Switch between grid view and list view.""" logger.debug("Switching view") self.icon_type = self.get_icon_type(view_type) self.view.destroy() self.view = load_view( view_type, get_game_list(filter_installed=self.filter_installed), filter_text=self.search_entry.get_text(), icon_type=self.icon_type) self.view.contextual_menu = self.menu self.connect_signals() self.games_scrollwindow.add(self.view) self.view.show_all() self.view.check_resize() # Note: set_active(True *or* False) apparently makes ALL the menuitems # in the group send the activate signal... if self.icon_type == 'banner_small': self.banner_small_menuitem.set_active(True) if self.icon_type == 'icon': self.icon_menuitem.set_active(True) if self.icon_type == 'banner': self.banner_menuitem.set_active(True) def on_icon_type_activate(self, menuitem): icon_type = menuitem.get_name() if icon_type == self.view.icon_type or not menuitem.get_active(): return if self.current_view_type == 'grid': settings.write_setting('icon_type_gridview', icon_type) elif self.current_view_type == 'list': settings.write_setting('icon_type_listview', icon_type) self.switch_view(self.current_view_type) def create_menu_shortcut(self, *args): """Add the game to the system's Games menu.""" game_slug = slugify(self.view.selected_game) create_launcher(game_slug, menu=True) dialogs.NoticeDialog( "Shortcut added to the Games category of the global menu.") def create_desktop_shortcut(self, *args): """Add the game to the system's Games menu.""" game_slug = slugify(self.view.selected_game) create_launcher(game_slug, desktop=True) dialogs.NoticeDialog('Shortcut created on your desktop.')
def _write_config(self): """Write the game configuration in the DB and config file. This needs to be unfucked """ if self.extends: logger.info( "This is an extension to %s, not creating a new game entry", self.extends, ) return configpath = make_game_config_id(self.slug) config_filename = os.path.join(settings.CONFIG_DIR, "games/%s.yml" % configpath) if self.requires: # Load the base game config required_game = pga.get_game_by_field(self.requires, field="installer_slug") base_config = LutrisConfig( runner_slug=self.runner, game_config_id=required_game["configpath"] ) config = base_config.game_level else: config = {"game": {}} self.game_id = pga.add_or_update( name=self.game_name, runner=self.runner, slug=self.game_slug, directory=self.target_path, installed=1, installer_slug=self.slug, parent_slug=self.requires, year=self.year, steamid=self.steamid, configpath=configpath, id=self.game_id, ) game = Game(self.game_id) game.set_platform_from_runner() game.save() logger.debug("Saved game entry %s (%d)", self.game_slug, self.game_id) # Config update if "system" in self.script: config["system"] = self._substitute_config(self.script["system"]) if self.runner in self.script and self.script[self.runner]: config[self.runner] = self._substitute_config(self.script[self.runner]) # Game options such as exe or main_file can be added at the root of the # script as a shortcut, this integrates them into the game config # properly launcher, launcher_value = _get_game_launcher(self.script) if isinstance(launcher_value, list): game_files = [] for game_file in launcher_value: if game_file in self.game_files: game_files.append(self.game_files[game_file]) else: game_files.append(game_file) config["game"][launcher] = game_files elif launcher_value: if launcher_value in self.game_files: launcher_value = self.game_files[launcher_value] elif self.target_path and os.path.exists( os.path.join(self.target_path, launcher_value) ): launcher_value = os.path.join(self.target_path, launcher_value) config["game"][launcher] = launcher_value if "game" in self.script: try: config["game"].update(self.script["game"]) except ValueError: raise ScriptingError("Invalid 'game' section", self.script["game"]) config["game"] = self._substitute_config(config["game"]) yaml_config = yaml.safe_dump(config, default_flow_style=False) with open(config_filename, "w") as config_file: config_file.write(yaml_config) if not self.extends: game.emit("game-installed")
def create_menu_shortcut(self, *args): """Add the selected game to the system's Games menu.""" game = Game(self.view.selected_game) xdg.create_launcher(game.slug, game.id, game.name, menu=True)
def do_command_line(self, command_line): # noqa: C901 # pylint: disable=arguments-differ # pylint: disable=too-many-locals,too-many-return-statements,too-many-branches # pylint: disable=too-many-statements # TODO: split into multiple methods to reduce complexity (35) options = command_line.get_options_dict() # Use stdout to output logs, only if no command line argument is # provided argc = len(sys.argv) - 1 if "-d" in sys.argv or "--debug" in sys.argv: argc -= 1 if not argc: # Switch back the log output to stderr (the default in Python) # to avoid messing with any output from command line options. # Use when targetting Python 3.7 minimum # console_handler.setStream(sys.stderr) # Until then... logger.removeHandler(log.console_handler) log.console_handler = logging.StreamHandler(stream=sys.stdout) log.console_handler.setFormatter(log.SIMPLE_FORMATTER) logger.addHandler(log.console_handler) # Set up logger if options.contains("debug"): log.console_handler.setFormatter(log.DEBUG_FORMATTER) logger.setLevel(logging.DEBUG) # Text only commands # Print Lutris version and exit if options.contains("version"): executable_name = os.path.basename(sys.argv[0]) print(executable_name + "-" + settings.VERSION) logger.setLevel(logging.NOTSET) return 0 logger.info("Lutris %s", settings.VERSION) migrate() run_all_checks() AsyncCall(init_dxvk_versions, None) # List game if options.contains("list-games"): game_list = games_db.get_games() if options.contains("installed"): game_list = [game for game in game_list if game["installed"]] if options.contains("json"): self.print_game_json(command_line, game_list) else: self.print_game_list(command_line, game_list) return 0 # List Steam games if options.contains("list-steam-games"): self.print_steam_list(command_line) return 0 # List Steam folders if options.contains("list-steam-folders"): self.print_steam_folders(command_line) return 0 # Execute command in Lutris context if options.contains("exec"): command = options.lookup_value("exec").get_string() self.execute_command(command) return 0 if options.contains("submit-issue"): IssueReportWindow(application=self) return 0 try: url = options.lookup_value(GLib.OPTION_REMAINING) installer_info = self.get_lutris_action(url) except ValueError: self._print(command_line, _("%s is not a valid URI") % url.get_strv()) return 1 game_slug = installer_info["game_slug"] action = installer_info["action"] if options.contains("output-script"): action = "write-script" revision = installer_info["revision"] installer_file = None if options.contains("install"): installer_file = options.lookup_value("install").get_string() if installer_file.startswith(("http:", "https:")): try: request = Request(installer_file).get() except HTTPError: self._print(command_line, _("Failed to download %s") % installer_file) return 1 try: headers = dict(request.response_headers) file_name = headers["Content-Disposition"].split("=", 1)[-1] except (KeyError, IndexError): file_name = os.path.basename(installer_file) file_path = os.path.join(tempfile.gettempdir(), file_name) self._print(command_line, _("download {url} to {file} started").format( url=installer_file, file=file_path)) with open(file_path, 'wb') as dest_file: dest_file.write(request.content) installer_file = file_path action = "install" else: installer_file = os.path.abspath(installer_file) action = "install" if not os.path.isfile(installer_file): self._print(command_line, _("No such file: %s") % installer_file) return 1 db_game = None if game_slug: if action == "rungameid": # Force db_game to use game id self.run_in_background = True db_game = games_db.get_game_by_field(game_slug, "id") elif action == "rungame": # Force db_game to use game slug self.run_in_background = True db_game = games_db.get_game_by_field(game_slug, "slug") elif action == "install": # Installers can use game or installer slugs self.run_in_background = True db_game = games_db.get_game_by_field(game_slug, "slug") \ or games_db.get_game_by_field(game_slug, "installer_slug") else: # Dazed and confused, try anything that might works db_game = ( games_db.get_game_by_field(game_slug, "id") or games_db.get_game_by_field(game_slug, "slug") or games_db.get_game_by_field(game_slug, "installer_slug") ) # If reinstall flag is passed, force the action to install if options.contains("reinstall"): action = "install" if action == "write-script": if not db_game or not db_game["id"]: logger.warning("No game provided to generate the script") return 1 self.generate_script(db_game, options.lookup_value("output-script").get_string()) return 0 # Graphical commands self.activate() self.set_tray_icon() if not action: if db_game and db_game["installed"]: # Game found but no action provided, ask what to do dlg = InstallOrPlayDialog(db_game["name"]) if not dlg.action_confirmed: action = None elif dlg.action == "play": action = "rungame" elif dlg.action == "install": action = "install" elif game_slug or installer_file: # No game found, default to install if a game_slug or # installer_file is provided action = "install" if action == "install": installers = get_installers( game_slug=game_slug, installer_file=installer_file, revision=revision, ) if installers: self.show_installer_window(installers) elif action in ("rungame", "rungameid"): if not db_game or not db_game["id"]: logger.warning("No game found in library") if not self.window.is_visible(): self.do_shutdown() return 0 game = Game(db_game["id"]) self.on_game_launch(game) return 0
def remove_menu_shortcut(self, *args): game = Game(self.view.selected_game) xdg.remove_launcher(game.slug, game.id, menu=True)
def launch_game(self, widget, _data=None): """Launch a game after it's been installed.""" widget.set_sensitive(False) self.on_destroy(widget) self.application.launch(Game(self.interpreter.installer.game_id))
def do_command_line(self, command_line): options = command_line.get_options_dict() if options.contains('debug'): logger.setLevel(logging.DEBUG) if options.contains('list-games'): game_list = pga.get_games() if options.contains('installed'): game_list = [game for game in game_list if game['installed']] if options.contains('json'): self.print_game_json(command_line, game_list) else: self.print_game_list(command_line, game_list) return 0 elif options.contains('list-steam-games'): self.print_steam_list(command_line) return 0 elif options.contains('list-steam-folders'): self.print_steam_folders(command_line) return 0 elif options.contains('exec'): command = options.lookup_value('exec').get_string() self.execute_command(command) return 0 game_slug = '' revision = None uri = options.lookup_value(GLib.OPTION_REMAINING) if uri: uri = uri.get_strv() if uri and len(uri): uri = uri[0] # TODO: Support multiple installer_info = parse_installer_url(uri) if installer_info is False: self._print(command_line, '%s is not a valid URI' % uri) return 1 game_slug = installer_info['game_slug'] revision = installer_info['revision'] if game_slug or options.contains('install'): installer_file = None if options.contains('install'): installer_file = options.lookup_value('install').get_string() if not os.path.isfile(installer_file): self._print(command_line, "No such file: %s" % installer_file) return 1 db_game = None if game_slug: db_game = (pga.get_game_by_field(game_slug, 'id') or pga.get_game_by_field(game_slug, 'slug') or pga.get_game_by_field(game_slug, 'installer_slug')) force_install = options.contains('reinstall') or bool(installer_info.get('revision')) if db_game and db_game['installed'] and not force_install: self._print(command_line, "Launching %s" % db_game['name']) if self.window: self.run_game(db_game['id']) else: lutris_game = Game(db_game['id']) # FIXME: This is awful lutris_game.exit_main_loop = True lutris_game.play() try: GLib.MainLoop().run() except KeyboardInterrupt: lutris_game.stop() else: self._print(command_line, "Installing %s" % game_slug or installer_file) if self.window: self.window.on_install_clicked(game_slug=game_slug, installer_file=installer_file, revision=revision) else: runtime_updater = RuntimeUpdater() runtime_updater.update() # FIXME: This should be a Gtk.Dialog child of LutrisWindow dialog = InstallerDialog(game_slug=game_slug, installer_file=installer_file, revision=revision) self.add_window(dialog) return 0 self.activate() return 0
def do_command_line(self, command_line): options = command_line.get_options_dict() # Use stdout to output logs, only if no command line argument is # provided argc = len(sys.argv) - 1 if "-d" in sys.argv or "--debug" in sys.argv: argc -= 1 if not argc: # Switch back the log output to stderr (the default in Python) # to avoid messing with any output from command line options. # Use when targetting Python 3.7 minimum # console_handler.setStream(sys.stderr) # Until then... logger.removeHandler(log.console_handler) log.console_handler = logging.StreamHandler(stream=sys.stdout) log.console_handler.setFormatter(log.SIMPLE_FORMATTER) logger.addHandler(log.console_handler) # Set up logger if options.contains("debug"): log.console_handler.setFormatter(log.DEBUG_FORMATTER) logger.setLevel(logging.DEBUG) # Text only commands # Print Lutris version and exit if options.contains("version"): executable_name = os.path.basename(sys.argv[0]) print(executable_name + "-" + settings.VERSION) logger.setLevel(logging.NOTSET) return 0 logger.info("Running Lutris %s", settings.VERSION) migrate() run_all_checks() AsyncCall(init_dxvk_versions) # List game if options.contains("list-games"): game_list = pga.get_games() if options.contains("installed"): game_list = [game for game in game_list if game["installed"]] if options.contains("json"): self.print_game_json(command_line, game_list) else: self.print_game_list(command_line, game_list) return 0 # List Steam games elif options.contains("list-steam-games"): self.print_steam_list(command_line) return 0 # List Steam folders elif options.contains("list-steam-folders"): self.print_steam_folders(command_line) return 0 # Execute command in Lutris context elif options.contains("exec"): command = options.lookup_value("exec").get_string() self.execute_command(command) return 0 try: url = options.lookup_value(GLib.OPTION_REMAINING) installer_info = self.get_lutris_action(url) except ValueError: self._print(command_line, "%s is not a valid URI" % url.get_strv()) return 1 game_slug = installer_info["game_slug"] action = installer_info["action"] revision = installer_info["revision"] installer_file = None if options.contains("install"): installer_file = options.lookup_value("install").get_string() installer_file = os.path.abspath(installer_file) action = "install" if not os.path.isfile(installer_file): self._print(command_line, "No such file: %s" % installer_file) return 1 # Graphical commands self.activate() self.set_tray_icon() db_game = None if game_slug: if action == "rungameid": # Force db_game to use game id db_game = pga.get_game_by_field(game_slug, "id") elif action == "rungame": # Force db_game to use game slug db_game = pga.get_game_by_field(game_slug, "slug") elif action == "install": # Installers can use game or installer slugs db_game = pga.get_game_by_field( game_slug, "slug") or pga.get_game_by_field( game_slug, "installer_slug") else: # Dazed and confused, try anything that might works db_game = (pga.get_game_by_field(game_slug, "id") or pga.get_game_by_field(game_slug, "slug") or pga.get_game_by_field(game_slug, "installer_slug")) if not action: if db_game and db_game["installed"]: # Game found but no action provided, ask what to do dlg = InstallOrPlayDialog(db_game["name"]) if not dlg.action_confirmed: action = None elif dlg.action == "play": action = "rungame" elif dlg.action == "install": action = "install" elif game_slug or installer_file: # No game found, default to install if a game_slug or # installer_file is provided action = "install" if action == "install": InstallerWindow( game_slug=game_slug, installer_file=installer_file, revision=revision, parent=self.window, application=self, ) elif action in ("rungame", "rungameid"): if not db_game or not db_game["id"]: logger.warning("No game found in library") return 0 logger.info("Launching %s", db_game["name"]) # If game is not installed, show the GUI before running. Otherwise leave the GUI closed. if not db_game["installed"]: self.window.present() self.launch(Game(db_game["id"])) else: self.window.present() return 0
def run(self): egs = get_game_by_field(self.client_installer, "slug") egs_game = Game(egs["id"]) egs_game.emit("game-launch")
class LutrisWindow(Gtk.Application): """Handler class for main window signals.""" def __init__(self, service=None): Gtk.Application.__init__( self, application_id="net.lutris.main", flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE) ui_filename = os.path.join(datapath.get(), 'ui', 'lutris-window.ui') if not os.path.exists(ui_filename): raise IOError('File %s not found' % ui_filename) self.service = service self.runtime_updater = RuntimeUpdater() self.running_game = None self.threads_stoppers = [] # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 self.game_selection_time = 0 self.game_launch_time = 0 self.last_selected_game = None self.selected_runner = None self.builder = Gtk.Builder() self.builder.add_from_file(ui_filename) # Load settings width = int(settings.read_setting('width') or 800) height = int(settings.read_setting('height') or 600) self.window_size = (width, height) window = self.builder.get_object('window') window.resize(width, height) view_type = self.get_view_type() self.load_icon_type_from_settings(view_type) self.filter_installed = \ settings.read_setting('filter_installed') == 'true' self.sidebar_visible = \ settings.read_setting('sidebar_visible') in ['true', None] # Set theme to dark if set in the settings dark_theme_menuitem = self.builder.get_object('dark_theme_menuitem') use_dark_theme = settings.read_setting('dark_theme') == 'true' dark_theme_menuitem.set_active(use_dark_theme) self.set_dark_theme(use_dark_theme) self.game_list = pga.get_games() # Load view self.game_store = GameStore([], self.icon_type, self.filter_installed) self.view = self.get_view(view_type) self.main_box = self.builder.get_object('main_box') self.splash_box = self.builder.get_object('splash_box') self.connect_link = self.builder.get_object('connect_link') # View menu installed_games_only_menuitem =\ self.builder.get_object('filter_installed') installed_games_only_menuitem.set_active(self.filter_installed) self.grid_view_menuitem = self.builder.get_object("gridview_menuitem") self.grid_view_menuitem.set_active(view_type == 'grid') self.list_view_menuitem = self.builder.get_object("listview_menuitem") self.list_view_menuitem.set_active(view_type == 'list') sidebar_menuitem = self.builder.get_object('sidebar_menuitem') sidebar_menuitem.set_active(self.sidebar_visible) # View buttons self.grid_view_btn = self.builder.get_object('switch_grid_view_btn') self.grid_view_btn.set_active(view_type == 'grid') self.list_view_btn = self.builder.get_object('switch_list_view_btn') self.list_view_btn.set_active(view_type == 'list') # Icon type menu self.banner_small_menuitem = \ self.builder.get_object('banner_small_menuitem') self.banner_small_menuitem.set_active(self.icon_type == 'banner_small') self.banner_menuitem = self.builder.get_object('banner_menuitem') self.banner_menuitem.set_active(self.icon_type == 'banner') self.icon_menuitem = self.builder.get_object('icon_menuitem') self.icon_menuitem.set_active(self.icon_type == 'icon') self.search_entry = self.builder.get_object('search_entry') self.search_entry.connect('icon-press', self.on_clear_search) # Scroll window self.games_scrollwindow = self.builder.get_object('games_scrollwindow') self.games_scrollwindow.add(self.view) # Buttons self.stop_button = self.builder.get_object('stop_button') self.stop_button.set_sensitive(False) self.delete_button = self.builder.get_object('delete_button') self.delete_button.set_sensitive(False) self.play_button = self.builder.get_object('play_button') self.play_button.set_sensitive(False) # Contextual menu main_entries = [ ('play', "Play", self.on_game_run), ('install', "Install", self.on_install_clicked), ('add', "Add manually", self.on_add_manually), ('configure', "Configure", self.on_edit_game_configuration), ('browse', "Browse files", self.on_browse_files), ('desktop-shortcut', "Create desktop shortcut", self.create_desktop_shortcut), ('rm-desktop-shortcut', "Delete desktop shortcut", self.remove_desktop_shortcut), ('menu-shortcut', "Create application menu shortcut", self.create_menu_shortcut), ('rm-menu-shortcut', "Delete application menu shortcut", self.remove_menu_shortcut), ('install_more', "Install (add) another version", self.on_install_clicked), ('remove', "Remove", self.on_remove_game), ] self.menu = ContextualMenu(main_entries) self.view.contextual_menu = self.menu # Sidebar self.sidebar_paned = self.builder.get_object('sidebar_paned') self.sidebar_treeview = SidebarTreeView() self.sidebar_treeview.connect('cursor-changed', self.on_sidebar_changed) self.sidebar_viewport = self.builder.get_object('sidebar_viewport') self.sidebar_viewport.add(self.sidebar_treeview) # Window initialization self.window = self.builder.get_object("window") self.window.resize_to_geometry(width, height) self.window.set_default_icon_name('lutris') self.window.show_all() self.builder.connect_signals(self) self.connect_signals() self.statusbar = self.builder.get_object("statusbar") # XXX Hide PGA config menu item until it actually gets implemented pga_menuitem = self.builder.get_object('pga_menuitem') pga_menuitem.hide() # Sync local lutris library with current Steam games before setting up # view steam.sync_with_lutris() self.game_store.fill_store(self.game_list) self.switch_splash_screen() self.show_sidebar() self.update_runtime() # Connect account and/or sync credentials = api.read_api_key() if credentials: self.on_connect_success(None, credentials) else: self.toggle_connection(False) self.sync_library() # Timers self.timer_ids = [GLib.timeout_add(300, self.refresh_status)] steamapps_paths = steam.get_steamapps_paths(flat=True) self.steam_watcher = steam.SteamWatcher(steamapps_paths, self.on_steam_game_changed) @property def current_view_type(self): return 'grid' \ if self.view.__class__.__name__ == "GameFlowBox" \ else 'list' def on_steam_game_changed(self, operation, path): appmanifest = steam.AppManifest(path) runner_name = appmanifest.get_runner_name() games = pga.get_game_by_field(appmanifest.steamid, field='steamid', all=True) if operation == 'DELETE': for game in games: if game['runner'] == runner_name: steam.mark_as_uninstalled(game) self.view.set_uninstalled(Game(game['id'])) break elif operation in ('MODIFY', 'CREATE'): if not appmanifest.is_installed(): return if runner_name == 'windows': return game_info = None for game in games: if game['installed'] == 0: game_info = game if not game_info: game_info = { 'name': appmanifest.name, 'slug': appmanifest.slug, } game_id = steam.mark_as_installed(appmanifest.steamid, runner_name, game_info) game_ids = [game['id'] for game in self.game_list] if game_id not in game_ids: self.add_game_to_view(game_id) else: self.view.set_installed(Game(game_id)) def set_dark_theme(self, is_dark): gtksettings = Gtk.Settings.get_default() gtksettings.set_property("gtk-application-prefer-dark-theme", is_dark) def get_view(self, view_type): if view_type == 'grid' and flowbox.FLOWBOX_SUPPORTED: return flowbox.GameFlowBox(self.game_list, icon_type=self.icon_type, filter_installed=self.filter_installed) else: return GameListView(self.game_store) def connect_signals(self): """Connect signals from the view with the main window. This must be called each time the view is rebuilt. """ self.view.connect('game-installed', self.on_game_installed) self.view.connect("game-activated", self.on_game_run) self.view.connect("game-selected", self.game_selection_changed) self.window.connect("configure-event", self.on_resize) def check_update(self): """Verify availability of client update.""" version_request = http.Request('https://lutris.net/version') version_request.get() version = version_request.content if version: latest_version = settings.read_setting('latest_version') if version > (latest_version or settings.VERSION): dialogs.ClientUpdateDialog() # Store latest version seen to avoid showing # the dialog more than once. settings.write_setting('latest_version', version) def get_view_type(self): if not flowbox.FLOWBOX_SUPPORTED: return 'list' view_type = settings.read_setting('view_type') if view_type in ['grid', 'list']: return view_type return settings.GAME_VIEW def load_icon_type_from_settings(self, view_type): """Return the icon style depending on the type of view.""" if view_type == 'list': self.icon_type = settings.read_setting('icon_type_listview') default = settings.ICON_TYPE_LISTVIEW else: self.icon_type = settings.read_setting('icon_type_gridview') default = settings.ICON_TYPE_GRIDVIEW if self.icon_type not in ("banner_small", "banner", "icon"): self.icon_type = default return self.icon_type def switch_splash_screen(self): if len(self.game_list) == 0: self.splash_box.show() self.sidebar_paned.hide() self.games_scrollwindow.hide() else: self.splash_box.hide() self.sidebar_paned.show() self.games_scrollwindow.show() def switch_view(self, view_type): """Switch between grid view and list view.""" self.view.destroy() self.load_icon_type_from_settings(view_type) self.game_store.set_icon_type(self.icon_type) self.view = self.get_view(view_type) self.view.contextual_menu = self.menu self.connect_signals() scrollwindow_children = self.games_scrollwindow.get_children() if len(scrollwindow_children): child = scrollwindow_children[0] child.destroy() self.games_scrollwindow.add(self.view) self.view.show_all() # Note: set_active(True *or* False) apparently makes ALL the menuitems # in the group send the activate signal... if self.icon_type == 'banner_small': self.banner_small_menuitem.set_active(True) elif self.icon_type == 'icon': self.icon_menuitem.set_active(True) elif self.icon_type == 'banner': self.banner_menuitem.set_active(True) settings.write_setting('view_type', view_type) def sync_library(self): """Synchronize games with local stuff and server.""" def update_gui(result, error): if result: added_ids, updated_ids = result added_games = pga.get_game_by_field(added_ids, 'id', all=True) self.game_list += added_games self.view.populate_games(added_games) self.switch_splash_screen() GLib.idle_add(self.update_existing_games, added_ids, updated_ids, True) else: logger.error("No results returned when syncing the library") self.set_status("Syncing library") AsyncCall(sync_from_remote, update_gui) def update_existing_games(self, added, updated, first_run=False): for game_id in updated.difference(added): self.view.update_row(pga.get_game_by_field(game_id, 'id')) if first_run: icons_sync = AsyncCall(self.sync_icons, callback=None) self.threads_stoppers.append(icons_sync.stop_request.set) self.set_status("") def update_runtime(self): self.runtime_updater.update(self.set_status) self.threads_stoppers += self.runtime_updater.cancellables def sync_icons(self): resources.fetch_icons([game['slug'] for game in self.game_list], callback=self.on_image_downloaded) def set_status(self, text): status_box = self.builder.get_object('status_box') for child_widget in status_box.get_children(): child_widget.destroy() label = Gtk.Label(text) label.show() status_box.add(label) def refresh_status(self): """Refresh status bar.""" if self.running_game: name = self.running_game.name if self.running_game.state == self.running_game.STATE_IDLE: self.set_status("Preparing to launch %s" % name) elif self.running_game.state == self.running_game.STATE_STOPPED: self.set_status("Game has quit") self.stop_button.set_sensitive(False) elif self.running_game.state == self.running_game.STATE_RUNNING: self.set_status("Playing %s" % name) self.stop_button.set_sensitive(True) return True # --------- # Callbacks # --------- def on_dark_theme_toggled(self, widget): use_dark_theme = widget.get_active() setting_value = 'true' if use_dark_theme else 'false' settings.write_setting('dark_theme', setting_value) self.set_dark_theme(use_dark_theme) def on_clear_search(self, widget, icon_pos, event): if icon_pos == Gtk.EntryIconPosition.SECONDARY: widget.set_text('') def on_connect(self, *args): """Callback when a user connects to his account.""" login_dialog = dialogs.ClientLoginDialog(self.window) login_dialog.connect('connected', self.on_connect_success) return True def on_connect_success(self, dialog, credentials): if isinstance(credentials, str): username = credentials else: username = credentials["username"] self.toggle_connection(True, username) self.sync_library() self.connect_link.hide() synchronize_menuitem = self.builder.get_object('synchronize_menuitem') synchronize_menuitem.set_sensitive(True) def on_disconnect(self, *args): api.disconnect() self.toggle_connection(False) self.connect_link.show() synchronize_menuitem = self.builder.get_object('synchronize_menuitem') synchronize_menuitem.set_sensitive(False) def toggle_connection(self, is_connected, username=None): disconnect_menuitem = self.builder.get_object('disconnect_menuitem') connect_menuitem = self.builder.get_object('connect_menuitem') connection_label = self.builder.get_object('connection_label') if is_connected: disconnect_menuitem.show() connect_menuitem.hide() connection_status = username logger.info('Connected to lutris.net as %s', connection_status) else: disconnect_menuitem.hide() connect_menuitem.show() connection_status = "Not connected" connection_label.set_text(connection_status) def on_games_button_clicked(self, widget): self._open_browser("https://lutris.net/games/") def on_register_account(self, *args): self._open_browser("https://lutris.net/user/register") def _open_browser(self, url): try: subprocess.check_call(["xdg-open", url]) except subprocess.CalledProcessError: Gtk.show_uri(None, url, Gdk.CURRENT_TIME) def on_synchronize_manually(self, widget): """Callback when Synchronize Library is activated.""" self.sync_library() def on_resize(self, widget, *args): """WTF is this doing?""" self.window_size = widget.get_size() def on_destroy(self, *args): """Signal for window close.""" # Stop cancellable running threads for stopper in self.threads_stoppers: stopper() self.steam_watcher.stop() if self.running_game \ and self.running_game.state != self.running_game.STATE_STOPPED: logger.info("%s is still running, stopping it", self.running_game.name) self.running_game.stop() if self.service: self.service.stop() # Save settings width, height = self.window_size settings.write_setting('width', width) settings.write_setting('height', height) Gtk.main_quit(*args) def on_runners_activate(self, _widget, _data=None): """Callback when manage runners is activated.""" RunnersDialog() def on_preferences_activate(self, _widget, _data=None): """Callback when preferences is activated.""" SystemConfigDialog(parent=self.window) def on_show_installed_games_toggled(self, widget, data=None): filter_installed = widget.get_active() setting_value = 'true' if filter_installed else 'false' settings.write_setting('filter_installed', setting_value) if self.current_view_type == 'grid': self.view.filter_installed = filter_installed self.view.invalidate_filter() else: self.game_store.filter_installed = filter_installed self.game_store.modelfilter.refilter() def on_pga_menuitem_activate(self, _widget, _data=None): dialogs.PgaSourceDialog(parent=self.window) def on_search_entry_changed(self, widget): if self.current_view_type == 'grid': self.view.filter_text = widget.get_text() self.view.invalidate_filter() else: self.game_store.filter_text = widget.get_text() self.game_store.modelfilter.refilter() def on_about_clicked(self, _widget, _data=None): """Open the about dialog.""" dialogs.AboutDialog(parent=self.window) def _get_current_game_id(self): """Return the id of the current selected game while taking care of the double clic bug. """ # Wait two seconds to avoid running a game twice if time.time() - self.game_launch_time < 2: return self.game_launch_time = time.time() return self.view.selected_game def on_game_run(self, _widget=None, game_id=None): """Launch a game, or install it if it is not""" if not game_id: game_id = self._get_current_game_id() if not game_id: return self.running_game = Game(game_id) if self.running_game.is_installed: self.running_game.play() else: game_slug = self.running_game.slug self.running_game = None InstallerDialog(game_slug, self) def on_game_stop(self, *args): """Stop running game.""" if self.running_game: self.running_game.stop() self.stop_button.set_sensitive(False) def on_install_clicked(self, _widget=None, game_ref=None): """Install a game""" if not game_ref: game_id = self._get_current_game_id() game = pga.get_game_by_field(game_id, 'id') game_ref = game.get('slug') logger.debug("Installing game %s (%s)" % (game_ref, game_id)) if not game_ref: return InstallerDialog(game_ref, self) def game_selection_changed(self, _widget): # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 if type(self.view) is GameGridView: is_double_click = time.time() - self.game_selection_time < 0.4 is_same_game = self.view.selected_game == self.last_selected_game if is_double_click and is_same_game: self.on_game_run() self.game_selection_time = time.time() self.last_selected_game = self.view.selected_game sensitive = True if self.view.selected_game else False self.play_button.set_sensitive(sensitive) self.delete_button.set_sensitive(sensitive) def on_game_installed(self, view, game_id): if type(game_id) != int: raise ValueError("game_id must be an int") if not self.view.has_game_id(game_id): logger.debug("Adding new installed game to view (%d)" % game_id) self.add_game_to_view(game_id, async=False) game = Game(game_id) view.set_installed(game) self.sidebar_treeview.update() GLib.idle_add(resources.fetch_icons, [game.slug], self.on_image_downloaded) def on_image_downloaded(self, game_slugs): for game_slug in game_slugs: games = pga.get_game_by_field(game_slug, field='slug', all=True) for game in games: game = Game(game['id']) is_installed = game.is_installed self.view.update_image(game.id, is_installed) def on_add_manually(self, widget, *args): def on_game_added(game): self.view.set_installed(game) self.sidebar_treeview.update() game = Game(self.view.selected_game) AddGameDialog(self.window, game=game, runner=self.selected_runner, callback=lambda: on_game_added(game)) def on_view_game_log_activate(self, widget): if not self.running_game: dialogs.ErrorDialog('No game log available') return log_title = u"Log for {}".format(self.running_game) log_window = LogWindow(log_title, self.window) log_window.logtextview.set_text(self.running_game.game_log) log_window.run() log_window.destroy() def on_add_game_button_clicked(self, _widget, _data=None): """Add a new game manually with the AddGameDialog.""" dialog = AddGameDialog( self.window, runner=self.selected_runner, callback=lambda: self.add_game_to_view(dialog.game.id)) return True def add_game_to_view(self, game_id, async=True): if not game_id: raise ValueError("Missing game id") def do_add_game(): self.view.add_game_by_id(game_id) self.switch_splash_screen() self.sidebar_treeview.update() if async: GLib.idle_add(do_add_game) else: do_add_game()
def on_game_installed(self, view, slug): view.set_installed(Game(slug))
def launch_game(self, widget, _data=None): """Launch a game after it's been installed.""" widget.set_sensitive(False) self.on_destroy(widget) game = Game(self.interpreter.installer.game_id) game.emit("game-launch")
class UninstallGameDialog(GtkBuilderDialog): glade_file = 'dialog-uninstall-game.ui' dialog_object = 'uninstall-game-dialog' def substitute_label(self, widget, name, replacement): if hasattr(widget, 'get_text'): get_text = widget.get_text set_text = widget.set_text elif hasattr(widget, 'get_label'): get_text = widget.get_label set_text = widget.set_label else: raise TypeError("Unsupported type %s" % type(widget)) replacement = replacement.replace('&', '&') set_text(get_text().replace("{%s}" % name, replacement)) def initialize(self, slug=None, callback=None): self.game = Game(slug) self.callback = callback runner = self.game.runner self.substitute_label(self.builder.get_object('description_label'), 'game', self.game.name) self.substitute_label( self.builder.get_object('remove_from_library_button'), 'game', self.game.name ) remove_contents_button = self.builder.get_object( 'remove_contents_button' ) if self.game.is_installed: if hasattr(runner, 'own_game_remove_method'): remove_contents_button.set_label(runner.own_game_remove_method) else: try: default_path = runner.default_path except AttributeError: default_path = "/" if not is_removeable(runner.game_path, excludes=[default_path]): remove_contents_button.set_sensitive(False) path = self.game.directory or 'disk' self.substitute_label(remove_contents_button, 'path', path) remove_contents_button.get_children()[0].set_use_markup(True) else: remove_contents_button.hide() cancel_button = self.builder.get_object('cancel_button') cancel_button.connect('clicked', self.on_close) apply_button = self.builder.get_object('apply_button') apply_button.connect('clicked', self.on_apply_button_clicked) def on_apply_button_clicked(self, widget): widget.set_sensitive(False) remove_from_library_button = self.builder.get_object( 'remove_from_library_button' ) remove_from_library = remove_from_library_button.get_active() remove_contents_button = self.builder.get_object( 'remove_contents_button' ) remove_contents = remove_contents_button.get_active() if remove_contents and not hasattr(self.game.runner, 'no_game_remove_warning'): game_dir = self.game.directory.replace('&', '&') dlg = QuestionDialog({ 'question': "Are you sure you want to delete EVERYTHING under " "\n<b>%s</b>?\n (This can't be undone)" % game_dir, 'title': "CONFIRM DANGEROUS OPERATION" }) if dlg.result != Gtk.ResponseType.YES: widget.set_sensitive(True) return self.game.remove(remove_from_library, remove_contents) self.callback(self.game.slug, remove_from_library) self.on_close()
class GameActions: """Regroup a list of callbacks for a game""" def __init__(self, application=None, window=None): self.application = application or Gio.Application.get_default() self.window = window self.game_id = None self._game = None @property def game(self): if not self._game: self._game = self.application.get_game_by_id(self.game_id) if not self._game: self._game = Game(self.game_id) self._game.connect("game-error", self.window.on_game_error) return self._game @property def is_game_running(self): return bool(self.application.get_game_by_id(self.game_id)) def set_game(self, game=None, game_id=None): if game: self._game = game self.game_id = game.id else: self._game = None self.game_id = game_id def get_game_actions(self): """Return a list of game actions and their callbacks""" return [ ("play", "Play", self.on_game_run), ("stop", "Stop", self.on_stop), ("show_logs", "Show logs", self.on_show_logs), ("install", "Install", self.on_install_clicked), ("add", "Add installed game", self.on_add_manually), ("configure", "Configure", self.on_edit_game_configuration), ("execute-script", "Execute script", self.on_execute_script_clicked), ("browse", "Browse files", self.on_browse_files), ( "desktop-shortcut", "Create desktop shortcut", self.on_create_desktop_shortcut, ), ( "rm-desktop-shortcut", "Delete desktop shortcut", self.on_remove_desktop_shortcut, ), ( "menu-shortcut", "Create application menu shortcut", self.on_create_menu_shortcut, ), ( "rm-menu-shortcut", "Delete application menu shortcut", self.on_remove_menu_shortcut, ), ("install_more", "Install another version", self.on_install_clicked), ("remove", "Remove", self.on_remove_game), ("view", "View on Lutris.net", self.on_view_game), ] def get_displayed_entries(self): """Return a dictionary of actions that should be shown for a game""" return { "add": not self.game.is_installed and not self.game.is_search_result, "install": not self.game.is_installed, "play": self.game.is_installed and not self.is_game_running, "stop": self.is_game_running, "show_logs": self.game.is_installed, "configure": bool(self.game.is_installed), "install_more": self.game.is_installed and not self.game.is_search_result, "execute-script": bool(self.game.is_installed and self.game.runner.system_config.get("manual_command")), "desktop-shortcut": (self.game.is_installed and not xdgshortcuts.desktop_launcher_exists( self.game.slug, self.game.id)), "menu-shortcut": (self.game.is_installed and not xdgshortcuts.menu_launcher_exists( self.game.slug, self.game.id)), "rm-desktop-shortcut": bool(self.game.is_installed and xdgshortcuts.desktop_launcher_exists( self.game.slug, self.game.id)), "rm-menu-shortcut": bool(self.game.is_installed and xdgshortcuts.menu_launcher_exists( self.game.slug, self.game.id)), "browse": self.game.is_installed and self.game.runner_name != "browser", "remove": not self.game.is_search_result, "view": True } def get_disabled_entries(self): """Return a dictionary of actions that should be disabled for a game""" return { "show_logs": not bool(self.game.game_thread), } def on_game_run(self, *_args): """Launch a game""" self.application.launch(self.game) def get_running_game(self): for i in range(self.application.running_games.get_n_items()): game = self.application.running_games.get_item(i) if game == self.game: return game def on_stop(self, caller): """Stops the game""" matched_game = self.get_running_game() if not matched_game: logger.warning("%s not in running game list", self.game_id) return try: os.kill(matched_game.game_thread.game_process.pid, signal.SIGTERM) except ProcessLookupError: pass logger.debug("Removed game with ID %s from running games", self.game_id) def on_show_logs(self, _widget): """Display game log""" return LogWindow(title="Log for {}".format(self.game), buffer=self.game.log_buffer, application=self.application) def on_install_clicked(self, *_args): """Install a game""" # Install the currently selected game in the UI return InstallerWindow( parent=self.window, game_slug=self.game.slug, application=self.application, ) def on_add_manually(self, _widget, *_args): """Callback that presents the Add game dialog""" AddGameDialog(self.window, game=self.game, runner=self.game.runner_name) def on_edit_game_configuration(self, _widget): """Edit game preferences""" EditGameConfigDialog(self.window, self.game) def on_execute_script_clicked(self, _widget): """Execute the game's associated script""" manual_command = self.game.runner.system_config.get("manual_command") if path_exists(manual_command): MonitoredCommand( [manual_command], include_processes=[os.path.basename(manual_command)], cwd=self.game.directory, ).start() logger.info("Running %s in the background", manual_command) def on_browse_files(self, _widget): """Callback to open a game folder in the file browser""" path = self.game.get_browse_dir() if not path: dialogs.NoticeDialog("This game has no installation directory") elif path_exists(path): if self.game.runner.system_config.get("use_xdg_utils"): subprocess.run(["xdg-open", path]) else: open_uri("file://%s" % path) else: dialogs.NoticeDialog("Can't open %s \nThe folder doesn't exist." % path) def on_create_menu_shortcut(self, *_args): """Add the selected game to the system's Games menu.""" xdgshortcuts.create_launcher(self.game.slug, self.game.id, self.game.name, menu=True) def on_create_desktop_shortcut(self, *_args): """Create a desktop launcher for the selected game.""" xdgshortcuts.create_launcher(self.game.slug, self.game.id, self.game.name, desktop=True) def on_remove_menu_shortcut(self, *_args): """Remove an XDG menu shortcut""" xdgshortcuts.remove_launcher(self.game.slug, self.game.id, menu=True) def on_remove_desktop_shortcut(self, *_args): """Remove a .desktop shortcut""" xdgshortcuts.remove_launcher(self.game.slug, self.game.id, desktop=True) def on_view_game(self, _widget): """Callback to open a game on lutris.net""" open_uri("https://lutris.net/games/%s" % self.game.slug) def on_remove_game(self, *_args): """Callback that present the uninstall dialog to the user""" UninstallGameDialog(game_id=self.game.id, callback=self.window.remove_game_from_view, parent=self.window)
class GameActions: """Regroup a list of callbacks for a game""" def __init__(self, application=None, window=None): self.application = application or Gio.Application.get_default() self.window = window self.game_id = None self._game = None @property def game(self): if not self._game: self._game = self.application.get_game_by_id(self.game_id) if not self._game: self._game = Game(self.game_id) self._game.connect("game-error", self.window.on_game_error) return self._game @property def is_game_running(self): return bool(self.application.get_game_by_id(self.game_id)) def set_game(self, game=None, game_id=None): if game: self._game = game self.game_id = game.id else: self._game = None self.game_id = game_id def get_game_actions(self): """Return a list of game actions and their callbacks""" return [ ( "play", "Play", self.on_game_run ), ( "stop", "Stop", self.on_stop ), ( "show_logs", "Show logs", self.on_show_logs ), ( "install", "Install", self.on_install_clicked ), ( "add", "Add installed game", self.on_add_manually ), ( "configure", "Configure", self.on_edit_game_configuration ), ( "execute-script", "Execute script", self.on_execute_script_clicked ), ( "browse", "Browse files", self.on_browse_files ), ( "desktop-shortcut", "Create desktop shortcut", self.on_create_desktop_shortcut, ), ( "rm-desktop-shortcut", "Delete desktop shortcut", self.on_remove_desktop_shortcut, ), ( "menu-shortcut", "Create application menu shortcut", self.on_create_menu_shortcut, ), ( "rm-menu-shortcut", "Delete application menu shortcut", self.on_remove_menu_shortcut, ), ( "install_more", "Install another version", self.on_install_clicked ), ( "remove", "Remove", self.on_remove_game ), ( "view", "View on Lutris.net", self.on_view_game ), ] def get_displayed_entries(self): """Return a dictionary of actions that should be shown for a game""" return { "add": not self.game.is_installed and not self.game.is_search_result, "install": not self.game.is_installed, "play": self.game.is_installed and not self.is_game_running, "stop": self.is_game_running, "show_logs": self.game.is_installed, "configure": bool(self.game.is_installed), "install_more": self.game.is_installed and not self.game.is_search_result, "execute-script": bool( self.game.is_installed and self.game.runner.system_config.get("manual_command") ), "desktop-shortcut": ( self.game.is_installed and not xdgshortcuts.desktop_launcher_exists(self.game.slug, self.game.id) ), "menu-shortcut": ( self.game.is_installed and not xdgshortcuts.menu_launcher_exists(self.game.slug, self.game.id) ), "rm-desktop-shortcut": bool( self.game.is_installed and xdgshortcuts.desktop_launcher_exists(self.game.slug, self.game.id) ), "rm-menu-shortcut": bool( self.game.is_installed and xdgshortcuts.menu_launcher_exists(self.game.slug, self.game.id) ), "browse": self.game.is_installed and self.game.runner_name != "browser", "remove": not self.game.is_search_result, "view": True } def on_game_run(self, *_args): """Launch a game""" self.application.launch(self.game) def get_running_game(self): for i in range(self.application.running_games.get_n_items()): game = self.application.running_games.get_item(i) if game == self.game: return game def on_stop(self, caller): """Stops the game""" matched_game = self.get_running_game() if not matched_game: logger.warning("%s not in running game list", self.game_id) return try: os.kill(matched_game.game_thread.game_process.pid, signal.SIGTERM) except ProcessLookupError: pass logger.debug("Removed game with ID %s from running games", self.game_id) def on_show_logs(self, _widget): """Display game log""" return LogWindow( title="Log for {}".format(self.game), buffer=self.game.log_buffer, application=self.application ) def on_install_clicked(self, *_args): """Install a game""" # Install the currently selected game in the UI return InstallerWindow( parent=self.window, game_slug=self.game.slug, application=self.application, ) def on_add_manually(self, _widget, *_args): """Callback that presents the Add game dialog""" AddGameDialog(self.window, game=self.game, runner=self.game.runner_name) def on_edit_game_configuration(self, _widget): """Edit game preferences""" EditGameConfigDialog(self.window, self.game) def on_execute_script_clicked(self, _widget): """Execute the game's associated script""" manual_command = self.game.runner.system_config.get("manual_command") if path_exists(manual_command): MonitoredCommand( [manual_command], include_processes=[os.path.basename(manual_command)], cwd=self.game.directory, ).start() logger.info("Running %s in the background", manual_command) def on_browse_files(self, _widget): """Callback to open a game folder in the file browser""" path = self.game.get_browse_dir() if not path: dialogs.NoticeDialog("This game has no installation directory") elif path_exists(path): open_uri("file://%s" % path) else: dialogs.NoticeDialog("Can't open %s \nThe folder doesn't exist." % path) def on_create_menu_shortcut(self, *_args): """Add the selected game to the system's Games menu.""" xdgshortcuts.create_launcher(self.game.slug, self.game.id, self.game.name, menu=True) def on_create_desktop_shortcut(self, *_args): """Create a desktop launcher for the selected game.""" xdgshortcuts.create_launcher(self.game.slug, self.game.id, self.game.name, desktop=True) def on_remove_menu_shortcut(self, *_args): """Remove an XDG menu shortcut""" xdgshortcuts.remove_launcher(self.game.slug, self.game.id, menu=True) def on_remove_desktop_shortcut(self, *_args): """Remove a .desktop shortcut""" xdgshortcuts.remove_launcher(self.game.slug, self.game.id, desktop=True) def on_view_game(self, _widget): """Callback to open a game on lutris.net""" open_uri("https://lutris.net/games/%s" % self.game.slug) def on_remove_game(self, *_args): """Callback that present the uninstall dialog to the user""" UninstallGameDialog( game_id=self.game.id, callback=self.window.remove_game_from_view, parent=self.window )
class LutrisWindow(Gtk.ApplicationWindow): """Handler class for main window signals.""" __gtype_name__ = 'LutrisWindow' main_box = GtkTemplate.Child() splash_box = GtkTemplate.Child() connect_link = GtkTemplate.Child() games_scrollwindow = GtkTemplate.Child() sidebar_paned = GtkTemplate.Child() sidebar_viewport = GtkTemplate.Child() statusbar = GtkTemplate.Child() connection_label = GtkTemplate.Child() status_box = GtkTemplate.Child() def __init__(self, application, **kwargs): self.runtime_updater = RuntimeUpdater() self.running_game = None self.threads_stoppers = [] # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 self.game_selection_time = 0 self.game_launch_time = 0 self.last_selected_game = None self.selected_runner = None self.selected_platform = None # Load settings width = int(settings.read_setting('width') or 800) height = int(settings.read_setting('height') or 600) self.window_size = (width, height) self.maximized = settings.read_setting('maximized') == 'True' view_type = self.get_view_type() self.load_icon_type_from_settings(view_type) self.filter_installed = \ settings.read_setting('filter_installed') == 'true' self.sidebar_visible = \ settings.read_setting('sidebar_visible') in ['true', None] self.use_dark_theme = settings.read_setting('dark_theme') == 'true' # Sync local lutris library with current Steam games and desktop games for service in get_services_synced_at_startup(): service.sync_with_lutris() # Window initialization self.game_list = pga.get_games() self.game_store = GameStore([], self.icon_type, self.filter_installed) self.view = self.get_view(view_type) super().__init__(default_width=width, default_height=height, icon_name='lutris', application=application, **kwargs) if self.maximized: self.maximize() self.init_template() self._init_actions() # Set theme to dark if set in the settings self.set_dark_theme(self.use_dark_theme) # Load view self.games_scrollwindow.add(self.view) self.connect_signals() self.view.show() # Contextual menu main_entries = [ ('play', "Play", self.on_game_run), ('install', "Install", self.on_install_clicked), ('add', "Add manually", self.on_add_manually), ('configure', "Configure", self.on_edit_game_configuration), ('browse', "Browse files", self.on_browse_files), ('desktop-shortcut', "Create desktop shortcut", self.create_desktop_shortcut), ('rm-desktop-shortcut', "Delete desktop shortcut", self.remove_desktop_shortcut), ('menu-shortcut', "Create application menu shortcut", self.create_menu_shortcut), ('rm-menu-shortcut', "Delete application menu shortcut", self.remove_menu_shortcut), ('install_more', "Install (add) another version", self.on_install_clicked), ('remove', "Remove", self.on_remove_game), ('view', "View on Lutris.net", self.on_view_game), ] self.menu = ContextualMenu(main_entries) self.view.contextual_menu = self.menu # Sidebar self.sidebar_treeview = SidebarTreeView() self.sidebar_treeview.connect('cursor-changed', self.on_sidebar_changed) self.sidebar_viewport.add(self.sidebar_treeview) self.sidebar_treeview.show() self.game_store.fill_store(self.game_list) self.switch_splash_screen() self.show_sidebar() self.update_runtime() # Connect account and/or sync credentials = api.read_api_key() if credentials: self.on_connect_success(None, credentials) else: self.toggle_connection(False) self.sync_library() # Timers self.timer_ids = [GLib.timeout_add(300, self.refresh_status)] steamapps_paths = steam.get_steamapps_paths(flat=True) self.steam_watcher = SteamWatcher(steamapps_paths, self.on_steam_game_changed) def _init_actions(self): Action = namedtuple( 'Action', ('callback', 'type', 'enabled', 'default', 'accel')) Action.__new__.__defaults__ = (None, None, True, None, None) actions = { 'browse-games': Action(lambda *x: open_uri('https://lutris.net/games/')), 'register-account': Action(lambda *x: open_uri('https://lutris.net/user/register/')), 'disconnect': Action(self.on_disconnect), 'connect': Action(self.on_connect), 'synchronize': Action(lambda *x: self.sync_library()), 'sync-local': Action(lambda *x: self.open_sync_dialog()), 'add-game': Action(self.on_add_game_button_clicked), 'view-game-log': Action(self.on_view_game_log_activate), 'stop-game': Action(self.on_game_stop, enabled=False), 'start-game': Action(self.on_game_run, enabled=False), 'remove-game': Action(self.on_remove_game, enabled=False), 'preferences': Action(self.on_preferences_activate), 'manage-runners': Action(lambda *x: RunnersDialog()), 'about': Action(self.on_about_clicked), 'show-installed-only': Action(self.on_show_installed_state_change, type='b', default=self.filter_installed, accel='<Primary>h'), 'view-type': Action(self.on_viewtype_state_change, type='s', default=self.current_view_type), 'icon-type': Action(self.on_icontype_state_change, type='s', default=self.icon_type), 'use-dark-theme': Action(self.on_dark_theme_state_change, type='b', default=self.use_dark_theme), 'show-side-bar': Action(self.on_sidebar_state_change, type='b', default=self.sidebar_visible, accel='F9'), } self.actions = {} app = self.props.application for name, value in actions.items(): if not value.type: action = Gio.SimpleAction.new(name) action.connect('activate', value.callback) else: default_value = None param_type = None if value.default is not None: default_value = GLib.Variant(value.type, value.default) if value.type != 'b': param_type = default_value.get_type() action = Gio.SimpleAction.new_stateful(name, param_type, default_value) action.connect('change-state', value.callback) self.actions[name] = action if value.enabled is False: action.props.enabled = False self.add_action(action) if value.accel: app.add_accelerator(value.accel, 'win.' + name) @property def current_view_type(self): return 'grid' if isinstance(self.view, GameGridView) else 'list' def on_steam_game_changed(self, operation, path): appmanifest = steam.AppManifest(path) if self.running_game and 'steam' in self.running_game.runner_name: self.running_game.notify_steam_game_changed(appmanifest) runner_name = appmanifest.get_runner_name() games = pga.get_games_where(steamid=appmanifest.steamid) if operation == Gio.FileMonitorEvent.DELETED: for game in games: if game['runner'] == runner_name: steam.mark_as_uninstalled(game) self.view.set_uninstalled(Game(game['id'])) break elif operation in (Gio.FileMonitorEvent.CHANGED, Gio.FileMonitorEvent.CREATED): if not appmanifest.is_installed(): return if runner_name == 'winesteam': return game_info = None for game in games: if game['installed'] == 0: game_info = game else: # Game is already installed, don't do anything return if not game_info: game_info = { 'name': appmanifest.name, 'slug': appmanifest.slug, } game_id = steam.mark_as_installed(appmanifest.steamid, runner_name, game_info) game_ids = [game['id'] for game in self.game_list] if game_id not in game_ids: self.add_game_to_view(game_id) else: self.view.set_installed(Game(game_id)) @staticmethod def set_dark_theme(is_dark): gtksettings = Gtk.Settings.get_default() gtksettings.set_property("gtk-application-prefer-dark-theme", is_dark) def get_view(self, view_type): if view_type == 'grid': return GameGridView(self.game_store) else: return GameListView(self.game_store) def connect_signals(self): """Connect signals from the view with the main window. This must be called each time the view is rebuilt. """ self.view.connect('game-installed', self.on_game_installed) self.view.connect("game-activated", self.on_game_run) self.view.connect("game-selected", self.game_selection_changed) self.view.connect("remove-game", self.on_remove_game) @staticmethod def check_update(): """Verify availability of client update.""" version_request = http.Request('https://lutris.net/version') version_request.get() version = version_request.content if version: latest_version = settings.read_setting('latest_version') if version > (latest_version or settings.VERSION): dialogs.ClientUpdateDialog() # Store latest version seen to avoid showing # the dialog more than once. settings.write_setting('latest_version', version) @staticmethod def get_view_type(): view_type = settings.read_setting('view_type') if view_type in ['grid', 'list']: return view_type return settings.GAME_VIEW def load_icon_type_from_settings(self, view_type): """Return the icon style depending on the type of view.""" if view_type == 'list': self.icon_type = settings.read_setting('icon_type_listview') default = settings.ICON_TYPE_LISTVIEW else: self.icon_type = settings.read_setting('icon_type_gridview') default = settings.ICON_TYPE_GRIDVIEW if self.icon_type not in ("banner_small", "banner", "icon", "icon_small"): self.icon_type = default return self.icon_type def switch_splash_screen(self): if len(self.game_list) == 0: self.splash_box.show() self.sidebar_paned.hide() self.games_scrollwindow.hide() else: self.splash_box.hide() self.sidebar_paned.show() self.games_scrollwindow.show() def switch_view(self, view_type): """Switch between grid view and list view.""" self.view.destroy() self.load_icon_type_from_settings(view_type) self.game_store.set_icon_type(self.icon_type) self.view = self.get_view(view_type) self.view.contextual_menu = self.menu self.connect_signals() scrollwindow_children = self.games_scrollwindow.get_children() if len(scrollwindow_children): child = scrollwindow_children[0] child.destroy() self.games_scrollwindow.add(self.view) self.set_selected_filter(self.selected_runner, self.selected_platform) self.set_show_installed_state(self.filter_installed) self.view.show_all() settings.write_setting('view_type', view_type) def sync_library(self): """Synchronize games with local stuff and server.""" def update_gui(result, error): if result: added_ids, updated_ids = result # sqlite limits the number of query parameters to 999, to # bypass that limitation, divide the query in chunks page_size = 999 added_games = chain.from_iterable([ pga.get_games_where( id__in=list(added_ids)[p * page_size:p * page_size + page_size]) for p in range(math.ceil(len(added_ids) / page_size)) ]) self.game_list += added_games self.view.populate_games(added_games) self.switch_splash_screen() GLib.idle_add(self.update_existing_games, added_ids, updated_ids, True) else: logger.error("No results returned when syncing the library") self.set_status("Syncing library") AsyncCall(sync_from_remote, update_gui) def open_sync_dialog(self): sync_dialog = SyncServiceDialog(parent=self) sync_dialog.run() def update_existing_games(self, added, updated, first_run=False): for game_id in updated.difference(added): # XXX this migth not work if the game has no 'item' set self.view.update_row(pga.get_game_by_field(game_id, 'id')) if first_run: icons_sync = AsyncCall(self.sync_icons, callback=None) self.threads_stoppers.append(icons_sync.stop_request.set) self.set_status("") def update_runtime(self): self.runtime_updater.update(self.set_status) self.threads_stoppers += self.runtime_updater.cancellables def sync_icons(self): resources.fetch_icons([game['slug'] for game in self.game_list], callback=self.on_image_downloaded) def set_status(self, text): for child_widget in self.status_box.get_children(): child_widget.destroy() label = Gtk.Label(text) label.show() self.status_box.add(label) def refresh_status(self): """Refresh status bar.""" if self.running_game: name = self.running_game.name if self.running_game.state == self.running_game.STATE_IDLE: self.set_status("Preparing to launch %s" % name) elif self.running_game.state == self.running_game.STATE_STOPPED: self.set_status("Game has quit") self.actions['stop-game'].props.enabled = False elif self.running_game.state == self.running_game.STATE_RUNNING: self.set_status("Playing %s" % name) self.actions['stop-game'].props.enabled = True return True # --------- # Callbacks # --------- def on_dark_theme_state_change(self, action, value): action.set_state(value) self.use_dark_theme = value.get_boolean() setting_value = 'true' if self.use_dark_theme else 'false' settings.write_setting('dark_theme', setting_value) self.set_dark_theme(self.use_dark_theme) @GtkTemplate.Callback def on_connect(self, *args): """Callback when a user connects to his account.""" login_dialog = dialogs.ClientLoginDialog(self) login_dialog.connect('connected', self.on_connect_success) return True def on_connect_success(self, dialog, credentials): if isinstance(credentials, str): username = credentials else: username = credentials["username"] self.toggle_connection(True, username) self.sync_library() self.connect_link.hide() self.actions['synchronize'].props.enabled = True @GtkTemplate.Callback def on_disconnect(self, *args): api.disconnect() self.toggle_connection(False) self.connect_link.show() self.actions['synchronize'].props.enabled = False def toggle_connection(self, is_connected, username=None): self.props.application.set_connect_state(is_connected) if is_connected: connection_status = username logger.info('Connected to lutris.net as %s', connection_status) else: connection_status = "Not connected" self.connection_label.set_text(connection_status) @GtkTemplate.Callback def on_resize(self, widget, *args): """Size-allocate signal. Updates stored window size and maximized state. """ if not widget.get_window(): return self.maximized = widget.is_maximized() if not self.maximized: self.window_size = widget.get_size() @GtkTemplate.Callback def on_destroy(self, *args): """Signal for window close.""" # Stop cancellable running threads for stopper in self.threads_stoppers: stopper() self.steam_watcher = None if self.running_game \ and self.running_game.state != self.running_game.STATE_STOPPED: logger.info("%s is still running, stopping it", self.running_game.name) self.running_game.stop() # Save settings width, height = self.window_size settings.write_setting('width', width) settings.write_setting('height', height) settings.write_setting('maximized', self.maximized) @GtkTemplate.Callback def on_preferences_activate(self, *args): """Callback when preferences is activated.""" SystemConfigDialog(parent=self) def on_show_installed_state_change(self, action, value): action.set_state(value) filter_installed = value.get_boolean() self.set_show_installed_state(filter_installed) def set_show_installed_state(self, filter_installed): self.filter_installed = filter_installed setting_value = 'true' if filter_installed else 'false' settings.write_setting('filter_installed', setting_value) self.game_store.filter_installed = filter_installed self.game_store.modelfilter.refilter() @GtkTemplate.Callback def on_pga_menuitem_activate(self, *args): dialogs.PgaSourceDialog(parent=self) @GtkTemplate.Callback def on_search_entry_changed(self, widget): self.game_store.filter_text = widget.get_text() self.game_store.modelfilter.refilter() @GtkTemplate.Callback def on_about_clicked(self, *args): """Open the about dialog.""" dialogs.AboutDialog(parent=self) def _get_current_game_id(self): """Return the id of the current selected game while taking care of the double clic bug. """ # Wait two seconds to avoid running a game twice if time.time() - self.game_launch_time < 2: return self.game_launch_time = time.time() return self.view.selected_game def on_game_run(self, *args, game_id=None): """Launch a game, or install it if it is not""" if not game_id: game_id = self._get_current_game_id() if not game_id: return self.running_game = Game(game_id) if self.running_game.is_installed: self.running_game.play() else: game_slug = self.running_game.slug self.running_game = None InstallerDialog(game_slug=game_slug, parent=self) @GtkTemplate.Callback def on_game_stop(self, *args): """Stop running game.""" if self.running_game: self.running_game.stop() self.actions['stop-game'].props.enabled = False def on_install_clicked(self, *args, game_slug=None, installer_file=None, revision=None): """Install a game""" installer_desc = game_slug if game_slug else installer_file if revision: installer_desc += " (%s)" % revision logger.info("Installing %s" % installer_desc) if not game_slug and not installer_file: # Install the currently selected game in the UI game_id = self._get_current_game_id() game = pga.get_game_by_field(game_id, 'id') game_slug = game.get('slug') if not game_slug and not installer_file: return InstallerDialog(game_slug=game_slug, installer_file=installer_file, revision=revision, parent=self) def game_selection_changed(self, _widget): # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 if isinstance(self.view, GameGridView): is_double_click = time.time() - self.game_selection_time < 0.4 is_same_game = self.view.selected_game == self.last_selected_game if is_double_click and is_same_game: self.on_game_run() self.game_selection_time = time.time() self.last_selected_game = self.view.selected_game sensitive = True if self.view.selected_game else False self.actions['start-game'].props.enabled = sensitive self.actions['remove-game'].props.enabled = sensitive def on_game_installed(self, view, game_id): if type(game_id) != int: raise ValueError("game_id must be an int") if not self.view.has_game_id(game_id): logger.debug("Adding new installed game to view (%d)" % game_id) self.add_game_to_view(game_id, async=False) game = Game(game_id) view.set_installed(game) self.sidebar_treeview.update() GLib.idle_add(resources.fetch_icons, [game.slug], self.on_image_downloaded) def on_image_downloaded(self, game_slugs): logger.debug("Updated images for %d games" % len(game_slugs)) for game_slug in game_slugs: games = pga.get_games_where(slug=game_slug) for game in games: game = Game(game['id']) is_installed = game.is_installed self.view.update_image(game.id, is_installed) def on_add_manually(self, widget, *args): def on_game_added(game): self.view.set_installed(game) self.sidebar_treeview.update() game = Game(self.view.selected_game) AddGameDialog(self, game=game, runner=self.selected_runner, callback=lambda: on_game_added(game)) @GtkTemplate.Callback def on_view_game_log_activate(self, *args): if not self.running_game: dialogs.ErrorDialog('No game log available', parent=self) return log_title = u"Log for {}".format(self.running_game) log_window = LogWindow(title=log_title, buffer=self.running_game.log_buffer, parent=self) log_window.run() log_window.destroy() @GtkTemplate.Callback def on_add_game_button_clicked(self, *args): """Add a new game manually with the AddGameDialog.""" dialog = AddGameDialog( self, runner=self.selected_runner, callback=lambda: self.add_game_to_view(dialog.game.id)) return True def add_game_to_view(self, game_id, async=True): if not game_id: raise ValueError("Missing game id") def do_add_game(): self.view.add_game_by_id(game_id) self.switch_splash_screen() self.sidebar_treeview.update() return False if async: GLib.idle_add(do_add_game) else: do_add_game()
class LutrisWindow(object): """Handler class for main window signals.""" def __init__(self): ui_filename = os.path.join(datapath.get(), "ui", "LutrisWindow.ui") if not os.path.exists(ui_filename): raise IOError("File %s not found" % ui_filename) self.running_game = None self.threads_stoppers = [] # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 self.game_selection_time = 0 self.game_launch_time = 0 self.last_selected_game = None self.builder = Gtk.Builder() self.builder.add_from_file(ui_filename) # Load settings width = int(settings.read_setting("width") or 800) height = int(settings.read_setting("height") or 600) self.window_size = (width, height) view_type = self.get_view_type() self.icon_type = self.get_icon_type(view_type) filter_installed_setting = settings.read_setting("filter_installed") or "false" filter_installed = filter_installed_setting == "true" show_installed_games_menuitem = self.builder.get_object("filter_installed") show_installed_games_menuitem.set_active(filter_installed) # Load view logger.debug("Loading view") self.game_store = GameStore([], self.icon_type, filter_installed) self.view = load_view(view_type, self.game_store) logger.debug("Connecting signals") self.main_box = self.builder.get_object("main_box") self.splash_box = self.builder.get_object("splash_box") # View menu self.grid_view_menuitem = self.builder.get_object("gridview_menuitem") self.grid_view_menuitem.set_active(view_type == "grid") self.list_view_menuitem = self.builder.get_object("listview_menuitem") self.list_view_menuitem.set_active(view_type == "list") # View buttons self.grid_view_btn = self.builder.get_object("switch_grid_view_btn") self.grid_view_btn.set_active(view_type == "grid") self.list_view_btn = self.builder.get_object("switch_list_view_btn") self.list_view_btn.set_active(view_type == "list") # Icon type menu self.banner_small_menuitem = self.builder.get_object("banner_small_menuitem") self.banner_small_menuitem.set_active(self.icon_type == "banner_small") self.banner_menuitem = self.builder.get_object("banner_menuitem") self.banner_menuitem.set_active(self.icon_type == "banner") self.icon_menuitem = self.builder.get_object("icon_menuitem") self.icon_menuitem.set_active(self.icon_type == "icon") self.search_entry = self.builder.get_object("search_entry") self.search_entry.connect("icon-press", self.on_clear_search) # Scroll window self.games_scrollwindow = self.builder.get_object("games_scrollwindow") self.games_scrollwindow.add(self.view) # Status bar self.status_label = self.builder.get_object("status_label") self.joystick_icons = [] # Buttons self.stop_button = self.builder.get_object("stop_button") self.stop_button.set_sensitive(False) self.delete_button = self.builder.get_object("delete_button") self.delete_button.set_sensitive(False) self.play_button = self.builder.get_object("play_button") self.play_button.set_sensitive(False) # Contextual menu main_entries = [ ("play", "Play", self.on_game_run), ("install", "Install", self.on_install_clicked), ("add", "Add manually", self.add_manually), ("configure", "Configure", self.edit_game_configuration), ("browse", "Browse files", self.on_browse_files), ("desktop-shortcut", "Create desktop shortcut", self.create_desktop_shortcut), ("rm-desktop-shortcut", "Delete desktop shortcut", self.remove_desktop_shortcut), ("menu-shortcut", "Create application menu shortcut", self.create_menu_shortcut), ("rm-menu-shortcut", "Delete application menu shortcut", self.remove_menu_shortcut), ("remove", "Remove", self.on_remove_game), ] self.menu = ContextualMenu(main_entries) self.view.contextual_menu = self.menu # Sidebar sidebar_paned = self.builder.get_object("sidebar_paned") sidebar_paned.set_position(150) self.sidebar_treeview = SidebarTreeView() self.sidebar_treeview.connect("cursor-changed", self.on_sidebar_changed) self.sidebar_viewport = self.builder.get_object("sidebar_viewport") self.sidebar_viewport.add(self.sidebar_treeview) # Window initialization self.window = self.builder.get_object("window") self.window.resize_to_geometry(width, height) self.window.show_all() self.builder.connect_signals(self) self.connect_signals() # XXX Hide PGA config menu item until it actually gets implemented pga_menuitem = self.builder.get_object("pga_menuitem") pga_menuitem.hide() self.init_game_store() # Connect account and/or sync credentials = api.read_api_key() if credentials: self.on_connect_success(None, credentials) else: self.toggle_connection(False) self.sync_library() # Timers self.timer_ids = [GLib.timeout_add(300, self.refresh_status), GLib.timeout_add(10000, self.on_sync_timer)] def init_game_store(self): logger.debug("Getting game list") game_list = get_game_list() self.game_store.fill_store(game_list) self.switch_splash_screen() @property def current_view_type(self): return "grid" if self.view.__class__.__name__ == "GameGridView" else "list" def connect_signals(self): """Connect signals from the view with the main window. This must be called each time the view is rebuilt. """ self.view.connect("game-installed", self.on_game_installed) self.view.connect("game-activated", self.on_game_run) self.view.connect("game-selected", self.game_selection_changed) self.window.connect("configure-event", self.on_resize) self.window.connect("key-press-event", self.on_keypress) def get_view_type(self): view_type = settings.read_setting("view_type") if view_type in ["grid", "list"]: return view_type return settings.GAME_VIEW def get_icon_type(self, view_type): """Return the icon style depending on the type of view.""" if view_type == "list": icon_type = settings.read_setting("icon_type_listview") default = settings.ICON_TYPE_LISTVIEW else: icon_type = settings.read_setting("icon_type_gridview") default = settings.ICON_TYPE_GRIDVIEW if icon_type not in ("banner_small", "banner", "icon"): icon_type = default return icon_type def switch_splash_screen(self): if not pga.get_table_length(): self.splash_box.show() self.games_scrollwindow.hide() self.sidebar_viewport.hide() else: self.splash_box.hide() self.games_scrollwindow.show() self.sidebar_viewport.show() def switch_view(self, view_type): """Switch between grid view and list view.""" logger.debug("Switching view") if view_type == self.get_view_type(): return self.view.destroy() icon_type = self.get_icon_type(view_type) self.game_store.set_icon_type(icon_type) self.view = load_view(view_type, self.game_store) self.view.contextual_menu = self.menu self.connect_signals() self.games_scrollwindow.add(self.view) self.view.show_all() # Note: set_active(True *or* False) apparently makes ALL the menuitems # in the group send the activate signal... if icon_type == "banner_small": self.banner_small_menuitem.set_active(True) if icon_type == "icon": self.icon_menuitem.set_active(True) if icon_type == "banner": self.banner_menuitem.set_active(True) settings.write_setting("view_type", view_type) def sync_library(self): """Synchronize games with local stuff and server.""" def update_gui(result, error): added, updated, installed, uninstalled = result self.switch_splash_screen() self.game_store.fill_store(added) GLib.idle_add(self.update_existing_games, added, updated, installed, uninstalled, True) self.set_status("Syncing library") AsyncCall(Sync().sync_all, update_gui) def update_existing_games(self, added, updated, installed, uninstalled, first_run=False): for game_id in updated.difference(added): self.view.update_row(pga.get_game_by_field(game_id, "id")) for game_id in installed.difference(added): if not self.view.get_row_by_id(game_id): self.view.add_game(game_id) self.view.set_installed(Game(game_id)) for game_id in uninstalled.difference(added): self.view.set_uninstalled(game_id) self.sidebar_treeview.update() if first_run: icons_sync = AsyncCall(self.sync_icons, None, stoppable=True) self.threads_stoppers.append(icons_sync.stop_request.set) self.set_status("Library synced") self.update_runtime() def update_runtime(self): cancellables = runtime.update(self.set_status) self.threads_stoppers += cancellables def sync_icons(self, stop_request=None): resources.fetch_icons( [game for game in pga.get_games()], callback=self.on_image_downloaded, stop_request=stop_request ) def set_status(self, text): self.status_label.set_text(text) def refresh_status(self): """Refresh status bar.""" if self.running_game: name = self.running_game.name if self.running_game.state == self.running_game.STATE_IDLE: self.set_status("Preparing to launch %s" % name) elif self.running_game.state == self.running_game.STATE_STOPPED: self.set_status("Game has quit") display.set_cursor("default", self.window.get_window()) self.stop_button.set_sensitive(False) elif self.running_game.state == self.running_game.STATE_RUNNING: self.set_status("Playing %s" % name) display.set_cursor("default", self.window.get_window()) self.stop_button.set_sensitive(True) for index in range(4): self.joystick_icons.append(self.builder.get_object("js" + str(index) + "image")) if os.path.exists("/dev/input/js%d" % index): self.joystick_icons[index].set_visible(True) else: self.joystick_icons[index].set_visible(False) return True def about(self, _widget, _data=None): """Open the about dialog.""" dialogs.AboutDialog() # Callbacks def on_clear_search(self, widget, icon_pos, event): if icon_pos == Gtk.EntryIconPosition.SECONDARY: widget.set_text("") def on_connect(self, *args): """Callback when a user connects to his account.""" login_dialog = dialogs.ClientLoginDialog() login_dialog.connect("connected", self.on_connect_success) def on_connect_success(self, dialog, credentials): if isinstance(credentials, str): username = credentials else: username = credentials["username"] self.toggle_connection(True, username) self.sync_library() def on_disconnect(self, *args): api.disconnect() self.toggle_connection(False) def toggle_connection(self, is_connected, username=None): disconnect_menuitem = self.builder.get_object("disconnect_menuitem") connect_menuitem = self.builder.get_object("connect_menuitem") connection_label = self.builder.get_object("connection_label") if is_connected: disconnect_menuitem.show() connect_menuitem.hide() connection_status = "Connected as %s" % username else: disconnect_menuitem.hide() connect_menuitem.show() connection_status = "Not connected" logger.info(connection_status) connection_label.set_text(connection_status) def on_register_account(self, *args): Gtk.show_uri(None, "http://lutris.net/user/register", Gdk.CURRENT_TIME) def on_synchronize_manually(self, *args): """Callback when Synchronize Library is activated.""" self.sync_library() def on_sync_timer(self): if not self.running_game or self.running_game.state == Game.STATE_STOPPED: def update_gui(result, error): self.update_existing_games(set(), set(), *result) AsyncCall(Sync().sync_local, update_gui) return True def on_resize(self, widget, *args): self.window_size = widget.get_size() def on_destroy(self, *args): """Signal for window close.""" # Stop cancellable running threads for stopper in self.threads_stoppers: stopper() # Save settings width, height = self.window_size settings.write_setting("width", width) settings.write_setting("height", height) Gtk.main_quit(*args) logger.debug("Quitting lutris") def on_runners_activate(self, _widget, _data=None): """Callback when manage runners is activated.""" RunnersDialog() def on_preferences_activate(self, _widget, _data=None): """Callback when preferences is activated.""" SystemConfigDialog() def on_show_installed_games_toggled(self, widget, data=None): filter_installed = widget.get_active() setting_value = "true" if filter_installed else "false" settings.write_setting("filter_installed", setting_value) self.game_store.filter_installed = filter_installed self.game_store.modelfilter.refilter() def on_pga_menuitem_activate(self, _widget, _data=None): dialogs.PgaSourceDialog() def on_search_entry_changed(self, widget): self.game_store.filter_text = widget.get_text() self.game_store.modelfilter.refilter() def _get_current_game_id(self): """Return the id of the current selected game while taking care of the double clic bug. """ # Wait two seconds to avoid running a game twice if time.time() - self.game_launch_time < 2: return self.game_launch_time = time.time() return self.view.selected_game def on_game_run(self, _widget=None, game_id=None): """Launch a game, or install it if it is not""" if not game_id: game_id = self._get_current_game_id() if not game_id: return display.set_cursor("wait", self.window.get_window()) self.running_game = Game(game_id) if self.running_game.is_installed: self.running_game.play() else: game_slug = self.running_game.slug self.running_game = None InstallerDialog(game_slug, self) def on_game_stop(self, *args): """Stop running game.""" if self.running_game: self.running_game.stop() self.stop_button.set_sensitive(False) def on_install_clicked(self, _widget=None, game_ref=None): """Install a game""" if not game_ref: game_id = self._get_current_game_id() game = pga.get_game_by_field(game_id, "id") game_ref = game.get("slug") logger.debug("Installing game %s (%s)" % (game_ref, game_id)) if not game_ref: return display.set_cursor("wait", self.window.get_window()) InstallerDialog(game_ref, self) def on_keypress(self, widget, event): if event.keyval == Gdk.KEY_F9: self.toggle_sidebar() def game_selection_changed(self, _widget): # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 if type(self.view) is GameGridView: is_double_click = time.time() - self.game_selection_time < 0.4 is_same_game = self.view.selected_game == self.last_selected_game if is_double_click and is_same_game: self.on_game_run() self.game_selection_time = time.time() self.last_selected_game = self.view.selected_game sensitive = True if self.view.selected_game else False self.play_button.set_sensitive(sensitive) self.delete_button.set_sensitive(sensitive) def on_game_installed(self, view, game_id): if not self.view.get_row_by_id(game_id): logger.debug("Adding new installed game to view (%s)" % game_id) self.add_game_to_view(game_id) view.set_installed(Game(game_id)) self.sidebar_treeview.update() def on_image_downloaded(self, game_id): game = Game(game_id) is_installed = game.is_installed self.view.update_image(game_id, is_installed) def add_manually(self, *args): game = Game(self.view.selected_game) add_game_dialog = AddGameDialog(self.window, game) add_game_dialog.run() if add_game_dialog.saved: self.view.set_installed(game) self.sidebar_treeview.update() def on_view_game_log_activate(self, widget): if not self.running_game: dialogs.ErrorDialog("No game log available") return log_title = u"Log for {}".format(self.running_game) log_window = LogWindow(log_title, self.window) log_window.logtextview.set_text(self.running_game.game_log) log_window.run() log_window.destroy() def add_game(self, _widget, _data=None): """Add a new game.""" add_game_dialog = AddGameDialog(self.window) add_game_dialog.run() if add_game_dialog.saved: self.add_game_to_view(add_game_dialog.game.id) def add_game_to_view(self, game_id): if not game_id: raise ValueError("Missing game id") def do_add_game(): self.view.add_game(game_id) self.switch_splash_screen() self.sidebar_treeview.update() GLib.idle_add(do_add_game) def on_remove_game(self, _widget, _data=None): selected_game = self.view.selected_game UninstallGameDialog(game_id=selected_game, callback=self.remove_game_from_view) def remove_game_from_view(self, game_id, from_library=False): def do_remove_game(): self.view.remove_game(game_id) self.switch_splash_screen() if from_library: GLib.idle_add(do_remove_game) else: self.view.update_image(game_id, is_installed=False) self.sidebar_treeview.update() def on_browse_files(self, widget): game = Game(self.view.selected_game) path = game.get_browse_dir() if path and os.path.exists(path): Gtk.show_uri(None, "file://" + path, Gdk.CURRENT_TIME) else: dialogs.NoticeDialog("Can't open %s \nThe folder doesn't exist." % path) def edit_game_configuration(self, _button): """Edit game preferences.""" def on_dialog_saved(): game_id = dialog.game.id self.view.remove_game(game_id) self.view.add_game(game_id) self.view.set_selected_game(game_id) self.sidebar_treeview.update() game = Game(self.view.selected_game) if game.is_installed: dialog = EditGameConfigDialog(self.window, game, on_dialog_saved) def on_viewmenu_toggled(self, menuitem): view_type = "grid" if menuitem.get_active() else "list" if view_type == self.current_view_type: return self.switch_view(view_type) self.grid_view_btn.set_active(view_type == "grid") self.list_view_btn.set_active(view_type == "list") def on_viewbtn_toggled(self, widget): view_type = "grid" if widget.get_active() else "list" if view_type == self.current_view_type: return self.switch_view(view_type) self.grid_view_menuitem.set_active(view_type == "grid") self.list_view_menuitem.set_active(view_type == "list") def on_icon_type_activate(self, menuitem): icon_type = menuitem.get_name() if icon_type == self.game_store.icon_type or not menuitem.get_active(): return if self.current_view_type == "grid": settings.write_setting("icon_type_gridview", icon_type) elif self.current_view_type == "list": settings.write_setting("icon_type_listview", icon_type) self.game_store.set_icon_type(icon_type) def create_menu_shortcut(self, *args): """Add the selected game to the system's Games menu.""" game = Game(self.view.selected_game).name shortcuts.create_launcher(game.slug, game.id, game.name, menu=True) def create_desktop_shortcut(self, *args): """Create a desktop launcher for the selected game.""" game = Game(self.view.selected_game) shortcuts.create_launcher(game.slug, game.id, game.name, desktop=True) def remove_menu_shortcut(self, *args): game = Game(self.view.selected_game) shortcuts.remove_launcher(game.slug, game.id, menu=True) def remove_desktop_shortcut(self, *args): game = Game(self.view.selected_game) shortcuts.remove_launcher(game.slug, game.id, desktop=True) def toggle_sidebar(self): if self.sidebar_viewport.is_visible(): self.sidebar_viewport.hide() else: self.sidebar_viewport.show() def on_sidebar_changed(self, widget): self.view.game_store.filter_runner = widget.get_selected_runner() self.game_store.modelfilter.refilter()
def on_view_game(self, widget): game = Game(self.view.selected_game) open_uri('https://lutris.net/games/' + game.slug)
def launch_game(self, widget, _data=None): """Launch a game after it's been installed""" widget.set_sensitive(False) game = Game(self.interpreter.game_slug) game.play() self.close(widget)
class GameDialogCommon(Dialog): """Base class for config dialogs""" no_runner_label = _("Select a runner in the Game Info tab") def __init__(self, title, parent=None): super().__init__(title, parent=parent) self.set_type_hint(Gdk.WindowTypeHint.NORMAL) self.set_default_size(DIALOG_WIDTH, DIALOG_HEIGHT) self.notebook = None self.name_entry = None self.runner_box = None self.timer_id = None self.game = None self.saved = None self.slug = None self.slug_entry = None self.directory_entry = None self.year_entry = None self.slug_change_button = None self.runner_dropdown = None self.banner_button = None self.icon_button = None self.game_box = None self.system_box = None self.runner_name = None self.runner_index = None self.lutris_config = None # These are independent windows, but start centered over # a parent like a dialog. Not modal, not really transient, # and does not share modality with other windows - so it # needs its own window group. Gtk.WindowGroup().add_window(self) GLib.idle_add(self.clear_transient_for) def clear_transient_for(self): # we need the parent set to be centered over the parent, but # we don't want to be transient really- we want other windows # able to come to the front. self.set_transient_for(None) return False @staticmethod def build_scrolled_window(widget): """Return a scrolled window containing config widgets""" scrolled_window = Gtk.ScrolledWindow(visible=True) scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolled_window.add(widget) return scrolled_window def build_notebook(self): self.notebook = Gtk.Notebook(visible=True) self.notebook.set_show_border(False) self.vbox.pack_start(self.notebook, True, True, 10) def build_tabs(self, config_level): """Build tabs (for game and runner levels)""" self.timer_id = None if config_level == "game": self._build_info_tab() self._build_game_tab() self._build_runner_tab(config_level) self._build_system_tab(config_level) def _build_info_tab(self): info_box = VBox() if self.game: info_box.pack_start(self._get_banner_box(), False, False, 6) # Banner info_box.pack_start(self._get_name_box(), False, False, 6) # Game name self.runner_box = self._get_runner_box() info_box.pack_start(self.runner_box, False, False, 6) # Runner info_box.pack_start(self._get_year_box(), False, False, 6) # Year if self.game: info_box.pack_start(self._get_slug_box(), False, False, 6) info_box.pack_start(self._get_directory_box(), False, False, 6) info_sw = self.build_scrolled_window(info_box) self._add_notebook_tab(info_sw, _("Game info")) def _get_name_box(self): box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) label = Label(_("Name")) box.pack_start(label, False, False, 0) self.name_entry = Gtk.Entry() if self.game: self.name_entry.set_text(self.game.name) box.pack_start(self.name_entry, True, True, 0) return box def _get_slug_box(self): slug_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) label = Label(_("Identifier")) slug_box.pack_start(label, False, False, 0) self.slug_entry = SlugEntry() self.slug_entry.set_text(self.game.slug) self.slug_entry.set_sensitive(False) self.slug_entry.connect("activate", self.on_slug_entry_activate) slug_box.pack_start(self.slug_entry, True, True, 0) self.slug_change_button = Gtk.Button(_("Change")) self.slug_change_button.connect("clicked", self.on_slug_change_clicked) slug_box.pack_start(self.slug_change_button, False, False, 0) return slug_box def _get_directory_box(self): """Return widget displaying the location of the game and allowing to move it""" box = Gtk.Box(spacing=12, margin_right=12, margin_left=12, visible=True) label = Label(_("Directory")) box.pack_start(label, False, False, 0) self.directory_entry = Gtk.Entry(visible=True) self.directory_entry.set_text(self.game.directory) self.directory_entry.set_sensitive(False) box.pack_start(self.directory_entry, True, True, 0) move_button = Gtk.Button(_("Move"), visible=True) move_button.connect("clicked", self.on_move_clicked) box.pack_start(move_button, False, False, 0) return box def _get_runner_box(self): runner_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) runner_label = Label(_("Runner")) runner_box.pack_start(runner_label, False, False, 0) self.runner_dropdown = self._get_runner_dropdown() runner_box.pack_start(self.runner_dropdown, True, True, 0) return runner_box def _get_banner_box(self): banner_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) label = Label("") banner_box.pack_start(label, False, False, 0) self.banner_button = Gtk.Button() self._set_image("banner") self.banner_button.connect("clicked", self.on_custom_image_select, "banner") banner_box.pack_start(self.banner_button, False, False, 0) reset_banner_button = Gtk.Button.new_from_icon_name("edit-clear", Gtk.IconSize.MENU) reset_banner_button.set_relief(Gtk.ReliefStyle.NONE) reset_banner_button.set_tooltip_text(_("Remove custom banner")) reset_banner_button.connect("clicked", self.on_custom_image_reset_clicked, "banner") banner_box.pack_start(reset_banner_button, False, False, 0) self.icon_button = Gtk.Button() self._set_image("icon") self.icon_button.connect("clicked", self.on_custom_image_select, "icon") banner_box.pack_start(self.icon_button, False, False, 0) reset_icon_button = Gtk.Button.new_from_icon_name("edit-clear", Gtk.IconSize.MENU) reset_icon_button.set_relief(Gtk.ReliefStyle.NONE) reset_icon_button.set_tooltip_text(_("Remove custom icon")) reset_icon_button.connect("clicked", self.on_custom_image_reset_clicked, "icon") banner_box.pack_start(reset_icon_button, False, False, 0) return banner_box def _get_year_box(self): box = Gtk.Box(spacing=12, margin_right=12, margin_left=12) label = Label(_("Release year")) box.pack_start(label, False, False, 0) self.year_entry = NumberEntry() if self.game: self.year_entry.set_text(str(self.game.year or "")) box.pack_start(self.year_entry, True, True, 0) return box def _set_image(self, image_format): image = Gtk.Image() service_media = LutrisBanner() if image_format == "banner" else LutrisIcon() game_slug = self.game.slug if self.game else "" image.set_from_pixbuf(service_media.get_pixbuf_for_game(game_slug)) if image_format == "banner": self.banner_button.set_image(image) else: self.icon_button.set_image(image) def _get_runner_dropdown(self): runner_liststore = self._get_runner_liststore() runner_dropdown = Gtk.ComboBox.new_with_model(runner_liststore) runner_dropdown.set_id_column(1) runner_index = 0 if self.runner_name: for runner in runner_liststore: if self.runner_name == str(runner[1]): break runner_index += 1 self.runner_index = runner_index runner_dropdown.set_active(self.runner_index) runner_dropdown.connect("changed", self.on_runner_changed) cell = Gtk.CellRendererText() cell.props.ellipsize = Pango.EllipsizeMode.END runner_dropdown.pack_start(cell, True) runner_dropdown.add_attribute(cell, "text", 0) return runner_dropdown @staticmethod def _get_runner_liststore(): """Build a ListStore with available runners.""" runner_liststore = Gtk.ListStore(str, str) runner_liststore.append((_("Select a runner from the list"), "")) for runner in runners.get_installed(): description = runner.description runner_liststore.append(("%s (%s)" % (runner.human_name, description), runner.name)) return runner_liststore def on_slug_change_clicked(self, widget): if self.slug_entry.get_sensitive() is False: widget.set_label(_("Apply")) self.slug_entry.set_sensitive(True) else: self.change_game_slug() def on_slug_entry_activate(self, _widget): self.change_game_slug() def change_game_slug(self): self.slug = self.slug_entry.get_text() self.slug_entry.set_sensitive(False) self.slug_change_button.set_label(_("Change")) def on_move_clicked(self, _button): new_location = DirectoryDialog("Select new location for the game", default_path=self.game.directory, parent=self) if not new_location.folder or new_location.folder == self.game.directory: return move_dialog = dialogs.MoveDialog(self.game, new_location.folder) move_dialog.connect("game-moved", self.on_game_moved) move_dialog.move() def on_game_moved(self, dialog): """Show a notification when the game is moved""" new_directory = dialog.new_directory if new_directory: self.directory_entry.set_text(new_directory) send_notification("Finished moving game", "%s moved to %s" % (dialog.game, new_directory)) else: send_notification("Failed to move game", "Lutris could not move %s" % dialog.game) def _build_game_tab(self): if self.game and self.runner_name: self.game.runner_name = self.runner_name if not self.game.runner or self.game.runner.name != self.runner_name: try: self.game.runner = runners.import_runner(self.runner_name)() except runners.InvalidRunner: pass self.game_box = GameBox(self.lutris_config, self.game) game_sw = self.build_scrolled_window(self.game_box) elif self.runner_name: game = Game(None) game.runner_name = self.runner_name self.game_box = GameBox(self.lutris_config, game) game_sw = self.build_scrolled_window(self.game_box) else: game_sw = Gtk.Label(label=self.no_runner_label) self._add_notebook_tab(game_sw, _("Game options")) def _build_runner_tab(self, _config_level): if self.runner_name: self.runner_box = RunnerBox(self.lutris_config, self.game) runner_sw = self.build_scrolled_window(self.runner_box) else: runner_sw = Gtk.Label(label=self.no_runner_label) self._add_notebook_tab(runner_sw, _("Runner options")) def _build_system_tab(self, _config_level): if not self.lutris_config: raise RuntimeError("Lutris config not loaded yet") self.system_box = SystemBox(self.lutris_config) self._add_notebook_tab( self.build_scrolled_window(self.system_box), _("System options") ) def _add_notebook_tab(self, widget, label): self.notebook.append_page(widget, Gtk.Label(label=label)) def build_action_area(self, button_callback): self.action_area.set_layout(Gtk.ButtonBoxStyle.EDGE) # Advanced settings checkbox checkbox = Gtk.CheckButton(label=_("Show advanced options")) if settings.read_setting("show_advanced_options") == "True": checkbox.set_active(True) checkbox.connect("toggled", self.on_show_advanced_options_toggled) self.action_area.pack_start(checkbox, False, False, 5) # Buttons hbox = Gtk.Box() cancel_button = Gtk.Button(label=_("Cancel")) cancel_button.connect("clicked", self.on_cancel_clicked) hbox.pack_start(cancel_button, True, True, 10) save_button = Gtk.Button(label=_("Save")) save_button.connect("clicked", button_callback) hbox.pack_start(save_button, True, True, 0) self.action_area.pack_start(hbox, True, True, 0) def on_show_advanced_options_toggled(self, checkbox): value = bool(checkbox.get_active()) settings.write_setting("show_advanced_options", value) self._set_advanced_options_visible(value) def _set_advanced_options_visible(self, value): """Change visibility of advanced options across all config tabs.""" widgets = self.system_box.get_children() if self.runner_name: widgets += self.runner_box.get_children() if self.game: widgets += self.game_box.get_children() for widget in widgets: if widget.get_style_context().has_class("advanced"): widget.set_visible(value) if value: widget.set_no_show_all(not value) widget.show_all() def on_runner_changed(self, widget): """Action called when runner drop down is changed.""" new_runner_index = widget.get_active() if self.runner_index and new_runner_index != self.runner_index: dlg = QuestionDialog( { "parent": self, "question": _("Are you sure you want to change the runner for this game ? " "This will reset the full configuration for this game and " "is not reversible."), "title": _("Confirm runner change"), } ) if dlg.result == Gtk.ResponseType.YES: self.runner_index = new_runner_index self._switch_runner(widget) else: # Revert the dropdown menu to the previously selected runner widget.set_active(self.runner_index) else: self.runner_index = new_runner_index self._switch_runner(widget) def _switch_runner(self, widget): """Rebuilds the UI on runner change""" current_page = self.notebook.get_current_page() if self.runner_index == 0: logger.info("No runner selected, resetting configuration") self.runner_name = None self.lutris_config = None else: runner_name = widget.get_model()[self.runner_index][1] if runner_name == self.runner_name: logger.debug("Runner unchanged, not creating a new config") return logger.info("Creating new configuration with runner %s", runner_name) self.runner_name = runner_name self.lutris_config = LutrisConfig(runner_slug=self.runner_name, level="game") self._rebuild_tabs() self.notebook.set_current_page(current_page) def _rebuild_tabs(self): for i in range(self.notebook.get_n_pages(), 1, -1): self.notebook.remove_page(i - 1) self._build_game_tab() self._build_runner_tab("game") self._build_system_tab("game") self.show_all() def on_cancel_clicked(self, _widget=None, _event=None): """Dialog destroy callback.""" if self.game: self.game.load_config() self.destroy() def is_valid(self): if not self.runner_name: ErrorDialog(_("Runner not provided"), parent=self) return False if not self.name_entry.get_text(): ErrorDialog(_("Please fill in the name"), parent=self) return False if self.runner_name == "steam" and not self.lutris_config.game_config.get("appid"): ErrorDialog(_("Steam AppID not provided"), parent=self) return False invalid_fields = [] runner_class = import_runner(self.runner_name) runner_instance = runner_class() for config in ["game", "runner"]: for k, v in getattr(self.lutris_config, config + "_config").items(): option = runner_instance.find_option(config + "_options", k) if option is None: continue validator = option.get("validator") if validator is not None: try: res = validator(v) logger.debug("%s validated successfully: %s", k, res) except Exception: invalid_fields.append(option.get("label")) if invalid_fields: ErrorDialog(_("The following fields have invalid values: ") + ", ".join(invalid_fields), parent=self) return False return True def on_save(self, _button): """Save game info and destroy widget. Return True if success.""" if not self.is_valid(): logger.warning(_("Current configuration is not valid, ignoring save request")) return False name = self.name_entry.get_text() if not self.slug: self.slug = slugify(name) if not self.game: self.game = Game() year = None if self.year_entry.get_text(): year = int(self.year_entry.get_text()) if not self.lutris_config.game_config_id: self.lutris_config.game_config_id = make_game_config_id(self.slug) runner_class = runners.import_runner(self.runner_name) runner = runner_class(self.lutris_config) self.game.name = name self.game.slug = self.slug self.game.year = year self.game.game_config_id = self.lutris_config.game_config_id self.game.runner = runner self.game.runner_name = self.runner_name self.game.is_installed = True self.game.config = self.lutris_config self.game.save(save_config=True) self.destroy() self.saved = True return True def on_custom_image_select(self, _widget, image_type): dialog = Gtk.FileChooserNative.new( _("Please choose a custom image"), self, Gtk.FileChooserAction.OPEN, None, None, ) image_filter = Gtk.FileFilter() image_filter.set_name(_("Images")) image_filter.add_pixbuf_formats() dialog.add_filter(image_filter) response = dialog.run() if response == Gtk.ResponseType.ACCEPT: image_path = dialog.get_filename() if image_type == "banner": self.game.has_custom_banner = True dest_path = os.path.join(settings.BANNER_PATH, "%s.jpg" % self.game.slug) size = BANNER_SIZE file_format = "jpeg" else: self.game.has_custom_icon = True dest_path = resources.get_icon_path(self.game.slug) size = ICON_SIZE file_format = "png" pixbuf = get_pixbuf(image_path, size) pixbuf.savev(dest_path, file_format, [], []) self._set_image(image_type) if image_type == "icon": system.update_desktop_icons() dialog.destroy() def on_custom_image_reset_clicked(self, _widget, image_type): if image_type == "banner": self.game.has_custom_banner = False dest_path = os.path.join(settings.BANNER_PATH, "%s.jpg" % self.game.slug) elif image_type == "icon": self.game.has_custom_icon = False dest_path = resources.get_icon_path(self.game.slug) else: raise ValueError("Unsupported image type %s" % image_type) if os.path.isfile(dest_path): os.remove(dest_path) self._set_image(image_type)