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 __init__(self, application, **kwargs): self.application = application 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.show_installed_first = \ settings.read_setting('show_installed_first') == '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( show_installed_first=self.show_installed_first) self.game_store = GameStore([], self.icon_type, self.filter_installed, self.show_installed_first) 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__(self, application, **kwargs): # pylint: disable=too-many-statements # TODO: refactor width = int(settings.read_setting("width") or self.default_width) height = int(settings.read_setting("height") or self.default_height) super().__init__( default_width=width, default_height=height, window_position=Gtk.WindowPosition.NONE, icon_name="lutris", application=application, **kwargs ) self.application = application self.runtime_updater = RuntimeUpdater() self.threads_stoppers = [] self.selected_runner = None self.selected_platform = None self.selected_category = None self.icon_type = None # Load settings 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) # Window initialization self.game_actions = GameActions(application=application, window=self) self.search_terms = None self.search_timer_id = None self.search_mode = "local" self.game_store = self.get_store() self.view = self.get_view(view_type) GObject.add_emission_hook(Game, "game-updated", self.on_game_updated) GObject.add_emission_hook(Game, "game-removed", self.on_game_updated) GObject.add_emission_hook(Game, "game-started", self.on_game_started) GObject.add_emission_hook(Game, "game-installed", self.on_game_installed) GObject.add_emission_hook(GenericPanel, "running-game-selected", self.game_selection_changed) self.connect("delete-event", self.on_window_delete) if self.maximized: self.maximize() self.init_template() self._init_actions() self._bind_zoom_adjustment() # Load view self.games_scrollwindow.add(self.view) self._connect_signals() # Set theme to dark if set in the settings self.set_dark_theme() self.set_viewtype_icon(view_type) # Add additional widgets lutris_icon = Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU) lutris_icon.set_margin_right(3) self.website_search_toggle.set_image(lutris_icon) self.website_search_toggle.set_label(_("Search Lutris.net")) self.website_search_toggle.set_tooltip_text(_("Search Lutris.net")) self.sidebar_listbox = SidebarListBox(self.application) self.sidebar_listbox.set_size_request(250, -1) self.sidebar_listbox.connect("selected-rows-changed", self.on_sidebar_changed) self.sidebar_scrolled.add(self.sidebar_listbox) self.game_panel = GenericPanel(application=self.application) self.game_scrolled = Gtk.ScrolledWindow(visible=True) self.game_scrolled.set_size_request(320, -1) self.game_scrolled.get_style_context().add_class("game-scrolled") self.game_scrolled.set_policy(Gtk.PolicyType.EXTERNAL, Gtk.PolicyType.EXTERNAL) self.game_scrolled.add(self.game_panel) self.panel_revealer = Gtk.Revealer(visible=True) self.panel_revealer.set_transition_duration(300) self.panel_revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_LEFT) self.panel_revealer.add(self.game_scrolled) self.main_box.pack_end(self.panel_revealer, False, False, 0) self.view.show() self.game_store.load() # Contextual menu self.view.contextual_menu = ContextualMenu(self.game_actions.get_game_actions()) # Left/Right Sidebar visibility self.sidebar_revealer.set_reveal_child(self.left_side_panel_visible) self.sidebar_revealer.set_transition_duration(300) self.panel_revealer.set_reveal_child(self.right_side_panel_visible) self.panel_revealer.set_transition_duration(300) self.update_runtime() # Connect account and/or sync credentials = api.read_api_key() if credentials: self.on_connect_success(None, credentials["username"]) else: self.toggle_connection(False) self.sync_library() self.sync_services()
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'): games = [] for game in game_list: games.append({ 'id': game['id'], 'slug': game['slug'], 'name': game['name'], 'runner': game['runner'], 'directory': game['directory'] }) self._print(command_line, json.dumps(games, indent=2)) else: for game in game_list: self._print( command_line, "{:4} | {:<40} | {:<40} | {:<15} | {:<64}".format( game['id'], game['name'][:40], game['slug'][:40], game['runner'] or '-', game['directory'] or '-' ) ) return 0 if options.contains('list-steam-games'): steamapps_paths = get_steamapps_paths() for platform in ('linux', 'windows'): for path in steamapps_paths[platform]: appmanifest_files = get_appmanifests(path) for appmanifest_file in appmanifest_files: appmanifest = AppManifest(os.path.join(path, appmanifest_file)) self._print( command_line, " {:8} | {:<60} | {:10} | {}".format( appmanifest.steamid, appmanifest.name or '-', platform, ", ".join(appmanifest.states) ) ) return 0 if options.contains('list-steam-folders'): steamapps_paths = get_steamapps_paths() for platform in ('linux', 'windows'): for path in steamapps_paths[platform]: self._print(command_line, path) return 0 check_config(force_wipe=False) migrate() game = None game_slug = '' uri = options.lookup_value(GLib.OPTION_REMAINING) if uri: uri = uri.get_strv() if uri and len(uri): uri = uri[0] # TODO: Support multiple if not uri.startswith('lutris:'): self._print(command_line, '%s is not a valid URI' % uri) return 1 game_slug = uri[7:] if game_slug or options.contains('install'): if options.contains('install'): installer_file = options.lookup_value('install').get_string() installer = installer_file else: installer_file = None installer = game_slug if not game_slug and 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')) if db_game and db_game['installed'] and not options.contains('reinstall'): 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() return 0 else: self._print(command_line, "Installing %s" % installer) if self.window: self.install_game(installer) else: runtime_updater = RuntimeUpdater() runtime_updater.update() # FIXME: This should be a Gtk.Dialog child of LutrisWindow dialog = InstallerDialog(installer) self.add_window(dialog) return 0 self.activate() return 0
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.application = application 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.show_installed_first = \ settings.read_setting('show_installed_first') == '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( show_installed_first=self.show_installed_first) self.game_store = GameStore([], self.icon_type, self.filter_installed, self.show_installed_first) 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'), 'show-installed-first': Action(self.on_show_installed_first_state_change, type='b', default=self.show_installed_first), '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, } if steam in get_services_synced_at_startup(): 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.connect('delete-event', lambda *x: self.hide_on_delete()) 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): try: resources.fetch_icons([game['slug'] for game in self.game_list], callback=self.on_image_downloaded) except TypeError as ex: logger.exception("Invalid game list:\n%s\nException: %s", self.game_list, ex) 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_first_state_change(self, action, value): action.set_state(value) show_installed_first = value.get_boolean() self.set_show_installed_first_state(show_installed_first) def set_show_installed_first_state(self, show_installed_first): self.show_installed_first = show_installed_first setting_value = 'true' if show_installed_first else 'false' settings.write_setting('show_installed_first', setting_value) self.game_store.sort_view(show_installed_first) self.game_store.modelfilter.refilter() 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 logger.warning("%s is not available", game_slug) self.running_game = None InstallerWindow(game_slug=game_slug, parent=self, application=self.application) @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""" logger.info("Installing %s%s", game_slug if game_slug else installer_file, " (%s)" % revision if revision else '') 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 return InstallerWindow(game_slug=game_slug, installer_file=installer_file, revision=revision, parent=self, application=self.application) 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, is_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, is_async=True): """Add a given game to the current view Params: game_id (str): SQL ID of the game to add is_async (bool): Adds the game asynchronously (defaults to 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 is_async: GLib.idle_add(do_add_game) else: do_add_game() @GtkTemplate.Callback def on_remove_game(self, *args): selected_game = self.view.selected_game UninstallGameDialog(game_id=selected_game, callback=self.remove_game_from_view, parent=self) 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): open_uri('file://' + path) else: dialogs.NoticeDialog("Can't open %s \nThe folder doesn't exist." % path) def on_view_game(self, widget): game = Game(self.view.selected_game) open_uri('https://lutris.net/games/' + game.slug) def on_edit_game_configuration(self, widget): """Edit game preferences.""" game = Game(self.view.selected_game) def on_dialog_saved(): game_id = dialog.game.id self.view.remove_game(game_id) self.view.add_game_by_id(game_id) self.view.set_selected_game(game_id) self.sidebar_treeview.update() if game.is_installed: dialog = EditGameConfigDialog(self, game, on_dialog_saved) def on_viewtype_state_change(self, action, val): action.set_state(val) view_type = val.get_string() if view_type != self.current_view_type: self.switch_view(view_type) def on_icontype_state_change(self, action, value): action.set_state(value) self.icon_type = value.get_string() if self.icon_type == self.game_store.icon_type: return if self.current_view_type == 'grid': settings.write_setting('icon_type_gridview', self.icon_type) elif self.current_view_type == 'list': settings.write_setting('icon_type_listview', self.icon_type) self.game_store.set_icon_type(self.icon_type) self.switch_view(self.get_view_type()) 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 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) def remove_menu_shortcut(self, *args): game = Game(self.view.selected_game) xdg.remove_launcher(game.slug, game.id, menu=True) def remove_desktop_shortcut(self, *args): game = Game(self.view.selected_game) xdg.remove_launcher(game.slug, game.id, desktop=True) def on_sidebar_state_change(self, action, value): action.set_state(value) self.sidebar_visible = value.get_boolean() if self.sidebar_visible: settings.write_setting('sidebar_visible', 'true') else: settings.write_setting('sidebar_visible', 'false') self.show_sidebar() def show_sidebar(self): width = 180 if self.sidebar_visible else 0 self.sidebar_paned.set_position(width) def on_sidebar_changed(self, widget): type, slug = widget.get_selected_filter() selected_runner = None selected_platform = None if not slug: pass elif type == 'platforms': selected_platform = slug elif type == 'runners': selected_runner = slug self.set_selected_filter(selected_runner, selected_platform) def set_selected_filter(self, runner, platform): self.selected_runner = runner self.selected_platform = platform self.game_store.filter_runner = self.selected_runner self.game_store.filter_platform = self.selected_platform self.game_store.modelfilter.refilter()
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 __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)
def __init__(self, application, **kwargs): width = int(settings.read_setting("width") or self.default_width) height = int(settings.read_setting("height") or self.default_height) super().__init__( default_width=width, default_height=height, icon_name="lutris", application=application, **kwargs ) self.application = application self.runtime_updater = RuntimeUpdater() self.threads_stoppers = [] self.selected_runner = None self.selected_platform = None self.icon_type = None # Load settings 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) # Window initialization self.game_actions = GameActions(application=application, window=self) self.game_store = self.get_store() self.view = self.get_view(view_type) GObject.add_emission_hook(Game, "game-updated", self.on_game_updated) GObject.add_emission_hook(GenericPanel, "game-searched", self.on_game_searched) GObject.add_emission_hook(GenericPanel, "running-game-selected", self.game_selection_changed) self.connect("delete-event", self.on_window_delete) if self.maximized: self.maximize() self.init_template() self._init_actions() self._bind_zoom_adjustment() # Load view self.games_scrollwindow.add(self.view) self._connect_signals() # Set theme to dark if set in the settings self.set_dark_theme() self.set_viewtype_icon(view_type) # Add additional widgets self.sidebar_listbox = SidebarListBox() self.sidebar_listbox.set_size_request(250, -1) self.sidebar_listbox.connect("selected-rows-changed", self.on_sidebar_changed) self.sidebar_scrolled.add(self.sidebar_listbox) self.game_panel = GenericPanel() self.game_scrolled = Gtk.ScrolledWindow(visible=True) self.game_scrolled.set_size_request(320, -1) self.game_scrolled.get_style_context().add_class("game-scrolled") self.game_scrolled.set_policy(Gtk.PolicyType.EXTERNAL, Gtk.PolicyType.EXTERNAL) self.game_scrolled.add(self.game_panel) self.panel_revealer = Gtk.Revealer(visible=True) self.panel_revealer.set_transition_duration(300) self.panel_revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_LEFT) self.panel_revealer.set_reveal_child(True) self.panel_revealer.add(self.game_scrolled) self.main_box.pack_end(self.panel_revealer, False, False, 0) self.view.show() self.game_store.load() # Contextual menu self.view.contextual_menu = ContextualMenu(self.game_actions.get_game_actions()) # Sidebar self.sidebar_revealer.set_reveal_child(self.sidebar_visible) self.sidebar_revealer.set_transition_duration(300) self.update_runtime() # Connect account and/or sync credentials = api.read_api_key() if credentials: self.on_connect_success(None, credentials["username"]) else: self.toggle_connection(False) self.sync_library() self.sync_services()
def __init__(self, application, **kwargs): width = int(settings.read_setting("width") or self.default_width) height = int(settings.read_setting("height") or self.default_height) super().__init__(default_width=width, default_height=height, window_position=Gtk.WindowPosition.NONE, icon_name="lutris", application=application, **kwargs) self.application = application self.runtime_updater = RuntimeUpdater() self.threads_stoppers = [] self.icon_type = None self.service = None self.window_size = (width, height) self.maximized = settings.read_setting("maximized") == "True" icon_type = self.load_icon_type() self.service_media = self.get_service_media(icon_type) self.game_actions = GameActions(application=application, window=self) self.search_timer_id = None self.game_store = None self.view = Gtk.Box() self.connect("delete-event", self.on_window_delete) self.connect("map-event", self.on_load) if self.maximized: self.maximize() self.init_template() self._init_actions() self.set_dark_theme() self.set_viewtype_icon(self.view_type) lutris_icon = Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU) lutris_icon.set_margin_right(3) self.selected_category = settings.read_setting("selected_category", default="runner:all") self.filters = self.load_filters() self.sidebar = LutrisSidebar(self.application, selected=self.selected_category) self.sidebar.set_size_request(250, -1) self.sidebar.connect("selected-rows-changed", self.on_sidebar_changed) self.sidebar_scrolled.add(self.sidebar) self.sidebar_revealer.set_reveal_child(self.left_side_panel_visible) self.sidebar_revealer.set_transition_duration(300) self.game_bar = None self.revealer_box = Gtk.HBox(visible=True) self.game_revealer.add(self.revealer_box) GObject.add_emission_hook(BaseService, "service-login", self.on_service_login) GObject.add_emission_hook(BaseService, "service-logout", self.on_service_logout) GObject.add_emission_hook(BaseService, "service-games-load", self.on_service_games_updating) GObject.add_emission_hook(BaseService, "service-games-loaded", self.on_service_games_updated)
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 # before setting up game list and view steam.sync_with_lutris() desktopapps.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 = steam.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: self._open_browser('https://lutris.net/games/') ), 'register-account': Action( lambda *x: self._open_browser('https://lutris.net/user/register') ), 'disconnect': Action(self.on_disconnect), 'connect': Action(self.on_connect), 'synchronize': Action(lambda *x: self.sync_library()), '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) runner_name = appmanifest.get_runner_name() games = pga.get_game_by_field(appmanifest.steamid, field='steamid', all=True) 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 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): # 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) @staticmethod def _open_browser(url): Gtk.show_uri(None, url, Gdk.CURRENT_TIME) @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""" if not game_slug: game_id = self._get_current_game_id() game = pga.get_game_by_field(game_id, 'id') game_slug = game.get('slug') logger.debug("Installing game %s (%s)" % (game_slug, game_id)) if not game_slug: 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): 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, 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(log_title, self) log_window.logtextview.set_text(self.running_game.game_log) 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()
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 # before setting up game list and view steam.sync_with_lutris() desktopapps.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 = steam.SteamWatcher(steamapps_paths, self.on_steam_game_changed)
def __init__(self, application, **kwargs): width = int(settings.read_setting("width") or self.default_width) height = int(settings.read_setting("height") or self.default_height) super().__init__(default_width=width, default_height=height, window_position=Gtk.WindowPosition.NONE, icon_name="lutris", application=application, **kwargs) self.application = application self.runtime_updater = RuntimeUpdater() self.threads_stoppers = [] self.icon_type = None self.service = None # Load settings self.window_size = (width, height) self.maximized = settings.read_setting("maximized") == "True" icon_type = self.load_icon_type() self.service_media = self.get_service_media(icon_type) # Window initialization self.game_actions = GameActions(application=application, window=self) self.search_timer_id = None self.game_store = None self.view = Gtk.Box() self.connect("delete-event", self.on_window_delete) self.connect("map-event", self.on_load) if self.maximized: self.maximize() self.init_template() self._init_actions() self._bind_zoom_adjustment() # Set theme to dark if set in the settings self.set_dark_theme() self.set_viewtype_icon(self.view_type) # Add additional widgets lutris_icon = Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU) lutris_icon.set_margin_right(3) self.selected_category = settings.read_setting("selected_category", default="runner:all") category, value = self.selected_category.split(":") self.filters = { category: value } # Type of filter corresponding to the selected sidebar element self.sidebar = LutrisSidebar(self.application, selected=self.selected_category) self.sidebar.set_size_request(250, -1) self.sidebar.connect("selected-rows-changed", self.on_sidebar_changed) self.sidebar_scrolled.add(self.sidebar) # Sidebar visibility self.sidebar_revealer.set_reveal_child(self.left_side_panel_visible) self.sidebar_revealer.set_transition_duration(300) self.game_bar = None self.service_bar = None self.revealer_box = Gtk.HBox(visible=True) self.game_revealer.add(self.revealer_box)
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)
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): # 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): 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 __init__(self, application, **kwargs): self.application = application self.runtime_updater = RuntimeUpdater() self.threads_stoppers = [] # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 self.game_launch_time = 0 self.selected_runner = None self.selected_platform = None self.icon_type = 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.show_installed_first = ( settings.read_setting("show_installed_first") == "true") self.sidebar_visible = settings.read_setting("sidebar_visible") in [ "true", None, ] self.view_sorting = settings.read_setting("view_sorting") or "name" self.view_sorting_ascending = ( settings.read_setting("view_sorting_ascending") != "false") self.use_dark_theme = (settings.read_setting( "dark_theme", default="false").lower() == "true") self.show_tray_icon = (settings.read_setting( "show_tray_icon", default="false").lower() == "true") # Window initialization self.game_actions = GameActions(application=application, window=self) self.game_list = pga.get_games( show_installed_first=self.show_installed_first) self.game_store = GameStore( [], self.icon_type, self.filter_installed, self.view_sorting, self.view_sorting_ascending, self.show_installed_first, ) self.view = self.get_view(view_type) self.game_store.connect("sorting-changed", self.on_game_store_sorting_changed) 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() self._bind_zoom_adjustment() # Load view self.games_scrollwindow.add(self.view) self._connect_signals() # Set theme to dark if set in the settings self.set_dark_theme(self.use_dark_theme) self.set_viewtype_icon(view_type) # Add additional widgets self.sidebar_listbox = SidebarListBox() self.sidebar_listbox.set_size_request(250, -1) self.sidebar_listbox.connect("selected-rows-changed", self.on_sidebar_changed) self.sidebar_scrolled.add(self.sidebar_listbox) self.game_revealer = Gtk.Revealer() self.game_revealer.show() self.game_revealer.set_transition_duration(500) self.game_revealer.set_transition_type( Gtk.RevealerTransitionType.SLIDE_LEFT) self.game_scrolled = Gtk.ScrolledWindow() self.game_scrolled.set_size_request(320, -1) self.game_scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) self.game_scrolled.show() self.game_revealer.add(self.game_scrolled) self.game_panel = Gtk.Box() self.main_box.pack_end(self.game_revealer, False, False, 0) self.view.show() # Contextual menu self.view.contextual_menu = ContextualMenu( self.game_actions.get_game_actions()) # Sidebar self.game_store.fill_store(self.game_list) self.switch_splash_screen() self.sidebar_revealer.set_reveal_child(self.sidebar_visible) self.update_runtime() # Connect account and/or sync credentials = api.read_api_key() if credentials: self.on_connect_success(None, credentials["username"]) else: self.toggle_connection(False) self.sync_library() self.sync_services()
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 check_config(force_wipe=False) migrate() game_slug = '' uri = options.lookup_value(GLib.OPTION_REMAINING) if uri: uri = uri.get_strv() if uri and len(uri): uri = uri[0] # TODO: Support multiple if not uri.startswith('lutris:'): self._print(command_line, '%s is not a valid URI' % uri) return 1 game_slug = uri[7:] if game_slug or options.contains('install'): if options.contains('install'): installer_file = options.lookup_value('install').get_string() installer = installer_file else: installer_file = None installer = game_slug if not game_slug and 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')) if db_game and db_game['installed'] and not options.contains( 'reinstall'): 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() return 0 else: self._print(command_line, "Installing %s" % installer) if self.window: self.install_game(installer) else: runtime_updater = RuntimeUpdater() runtime_updater.update() # FIXME: This should be a Gtk.Dialog child of LutrisWindow dialog = InstallerDialog(installer) self.add_window(dialog) return 0 self.activate() return 0