def __init__(self): ui_filename = os.path.join(datapath.get(), "ui", "LutrisWindow.ui") if not os.path.exists(ui_filename): raise IOError("File %s not found" % ui_filename) self.running_game = None self.threads_stoppers = [] # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 self.game_selection_time = 0 self.game_launch_time = 0 self.last_selected_game = None self.builder = Gtk.Builder() self.builder.add_from_file(ui_filename) # Load settings width = int(settings.read_setting("width") or 800) height = int(settings.read_setting("height") or 600) self.window_size = (width, height) view_type = self.get_view_type() self.icon_type = self.get_icon_type(view_type) filter_installed_setting = settings.read_setting("filter_installed") or "false" filter_installed = filter_installed_setting == "true" show_installed_games_menuitem = self.builder.get_object("filter_installed") show_installed_games_menuitem.set_active(filter_installed) # Load view logger.debug("Loading view") self.game_store = GameStore([], self.icon_type, filter_installed) self.view = load_view(view_type, self.game_store) logger.debug("Connecting signals") self.main_box = self.builder.get_object("main_box") self.splash_box = self.builder.get_object("splash_box") # View menu self.grid_view_menuitem = self.builder.get_object("gridview_menuitem") self.grid_view_menuitem.set_active(view_type == "grid") self.list_view_menuitem = self.builder.get_object("listview_menuitem") self.list_view_menuitem.set_active(view_type == "list") # View buttons self.grid_view_btn = self.builder.get_object("switch_grid_view_btn") self.grid_view_btn.set_active(view_type == "grid") self.list_view_btn = self.builder.get_object("switch_list_view_btn") self.list_view_btn.set_active(view_type == "list") # Icon type menu self.banner_small_menuitem = self.builder.get_object("banner_small_menuitem") self.banner_small_menuitem.set_active(self.icon_type == "banner_small") self.banner_menuitem = self.builder.get_object("banner_menuitem") self.banner_menuitem.set_active(self.icon_type == "banner") self.icon_menuitem = self.builder.get_object("icon_menuitem") self.icon_menuitem.set_active(self.icon_type == "icon") self.search_entry = self.builder.get_object("search_entry") self.search_entry.connect("icon-press", self.on_clear_search) # Scroll window self.games_scrollwindow = self.builder.get_object("games_scrollwindow") self.games_scrollwindow.add(self.view) # Status bar self.status_label = self.builder.get_object("status_label") self.joystick_icons = [] # Buttons self.stop_button = self.builder.get_object("stop_button") self.stop_button.set_sensitive(False) self.delete_button = self.builder.get_object("delete_button") self.delete_button.set_sensitive(False) self.play_button = self.builder.get_object("play_button") self.play_button.set_sensitive(False) # Contextual menu main_entries = [ ("play", "Play", self.on_game_run), ("install", "Install", self.on_install_clicked), ("add", "Add manually", self.add_manually), ("configure", "Configure", self.edit_game_configuration), ("browse", "Browse files", self.on_browse_files), ("desktop-shortcut", "Create desktop shortcut", self.create_desktop_shortcut), ("rm-desktop-shortcut", "Delete desktop shortcut", self.remove_desktop_shortcut), ("menu-shortcut", "Create application menu shortcut", self.create_menu_shortcut), ("rm-menu-shortcut", "Delete application menu shortcut", self.remove_menu_shortcut), ("remove", "Remove", self.on_remove_game), ] self.menu = ContextualMenu(main_entries) self.view.contextual_menu = self.menu # Sidebar sidebar_paned = self.builder.get_object("sidebar_paned") sidebar_paned.set_position(150) self.sidebar_treeview = SidebarTreeView() self.sidebar_treeview.connect("cursor-changed", self.on_sidebar_changed) self.sidebar_viewport = self.builder.get_object("sidebar_viewport") self.sidebar_viewport.add(self.sidebar_treeview) # Window initialization self.window = self.builder.get_object("window") self.window.resize_to_geometry(width, height) self.window.show_all() self.builder.connect_signals(self) self.connect_signals() # XXX Hide PGA config menu item until it actually gets implemented pga_menuitem = self.builder.get_object("pga_menuitem") pga_menuitem.hide() self.init_game_store() # Connect account and/or sync credentials = api.read_api_key() if credentials: self.on_connect_success(None, credentials) else: self.toggle_connection(False) self.sync_library() # Timers self.timer_ids = [GLib.timeout_add(300, self.refresh_status), GLib.timeout_add(10000, self.on_sync_timer)]
def __init__(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 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.sidebar_width = int(settings.read_setting('sidebar_width') or 180) 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' # 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), ('execute-script', "Execute script", self.on_execute_script_clicked), ('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) self.gui_needs_update = True self.config_menu_first_access = True
class LutrisWindow(object): """Handler class for main window signals.""" def __init__(self): ui_filename = os.path.join(datapath.get(), "ui", "LutrisWindow.ui") if not os.path.exists(ui_filename): raise IOError("File %s not found" % ui_filename) self.running_game = None self.threads_stoppers = [] # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 self.game_selection_time = 0 self.game_launch_time = 0 self.last_selected_game = None self.builder = Gtk.Builder() self.builder.add_from_file(ui_filename) # Load settings width = int(settings.read_setting("width") or 800) height = int(settings.read_setting("height") or 600) self.window_size = (width, height) view_type = self.get_view_type() self.icon_type = self.get_icon_type(view_type) filter_installed_setting = settings.read_setting("filter_installed") or "false" filter_installed = filter_installed_setting == "true" show_installed_games_menuitem = self.builder.get_object("filter_installed") show_installed_games_menuitem.set_active(filter_installed) # Load view logger.debug("Loading view") self.game_store = GameStore([], self.icon_type, filter_installed) self.view = load_view(view_type, self.game_store) logger.debug("Connecting signals") self.main_box = self.builder.get_object("main_box") self.splash_box = self.builder.get_object("splash_box") # View menu self.grid_view_menuitem = self.builder.get_object("gridview_menuitem") self.grid_view_menuitem.set_active(view_type == "grid") self.list_view_menuitem = self.builder.get_object("listview_menuitem") self.list_view_menuitem.set_active(view_type == "list") # View buttons self.grid_view_btn = self.builder.get_object("switch_grid_view_btn") self.grid_view_btn.set_active(view_type == "grid") self.list_view_btn = self.builder.get_object("switch_list_view_btn") self.list_view_btn.set_active(view_type == "list") # Icon type menu self.banner_small_menuitem = self.builder.get_object("banner_small_menuitem") self.banner_small_menuitem.set_active(self.icon_type == "banner_small") self.banner_menuitem = self.builder.get_object("banner_menuitem") self.banner_menuitem.set_active(self.icon_type == "banner") self.icon_menuitem = self.builder.get_object("icon_menuitem") self.icon_menuitem.set_active(self.icon_type == "icon") self.search_entry = self.builder.get_object("search_entry") self.search_entry.connect("icon-press", self.on_clear_search) # Scroll window self.games_scrollwindow = self.builder.get_object("games_scrollwindow") self.games_scrollwindow.add(self.view) # Status bar self.status_label = self.builder.get_object("status_label") self.joystick_icons = [] # Buttons self.stop_button = self.builder.get_object("stop_button") self.stop_button.set_sensitive(False) self.delete_button = self.builder.get_object("delete_button") self.delete_button.set_sensitive(False) self.play_button = self.builder.get_object("play_button") self.play_button.set_sensitive(False) # Contextual menu main_entries = [ ("play", "Play", self.on_game_run), ("install", "Install", self.on_install_clicked), ("add", "Add manually", self.add_manually), ("configure", "Configure", self.edit_game_configuration), ("browse", "Browse files", self.on_browse_files), ("desktop-shortcut", "Create desktop shortcut", self.create_desktop_shortcut), ("rm-desktop-shortcut", "Delete desktop shortcut", self.remove_desktop_shortcut), ("menu-shortcut", "Create application menu shortcut", self.create_menu_shortcut), ("rm-menu-shortcut", "Delete application menu shortcut", self.remove_menu_shortcut), ("remove", "Remove", self.on_remove_game), ] self.menu = ContextualMenu(main_entries) self.view.contextual_menu = self.menu # Sidebar sidebar_paned = self.builder.get_object("sidebar_paned") sidebar_paned.set_position(150) self.sidebar_treeview = SidebarTreeView() self.sidebar_treeview.connect("cursor-changed", self.on_sidebar_changed) self.sidebar_viewport = self.builder.get_object("sidebar_viewport") self.sidebar_viewport.add(self.sidebar_treeview) # Window initialization self.window = self.builder.get_object("window") self.window.resize_to_geometry(width, height) self.window.show_all() self.builder.connect_signals(self) self.connect_signals() # XXX Hide PGA config menu item until it actually gets implemented pga_menuitem = self.builder.get_object("pga_menuitem") pga_menuitem.hide() self.init_game_store() # Connect account and/or sync credentials = api.read_api_key() if credentials: self.on_connect_success(None, credentials) else: self.toggle_connection(False) self.sync_library() # Timers self.timer_ids = [GLib.timeout_add(300, self.refresh_status), GLib.timeout_add(10000, self.on_sync_timer)] def init_game_store(self): logger.debug("Getting game list") game_list = get_game_list() self.game_store.fill_store(game_list) self.switch_splash_screen() @property def current_view_type(self): return "grid" if self.view.__class__.__name__ == "GameGridView" else "list" def connect_signals(self): """Connect signals from the view with the main window. This must be called each time the view is rebuilt. """ self.view.connect("game-installed", self.on_game_installed) self.view.connect("game-activated", self.on_game_run) self.view.connect("game-selected", self.game_selection_changed) self.window.connect("configure-event", self.on_resize) self.window.connect("key-press-event", self.on_keypress) def get_view_type(self): view_type = settings.read_setting("view_type") if view_type in ["grid", "list"]: return view_type return settings.GAME_VIEW def get_icon_type(self, view_type): """Return the icon style depending on the type of view.""" if view_type == "list": icon_type = settings.read_setting("icon_type_listview") default = settings.ICON_TYPE_LISTVIEW else: icon_type = settings.read_setting("icon_type_gridview") default = settings.ICON_TYPE_GRIDVIEW if icon_type not in ("banner_small", "banner", "icon"): icon_type = default return icon_type def switch_splash_screen(self): if not pga.get_table_length(): self.splash_box.show() self.games_scrollwindow.hide() self.sidebar_viewport.hide() else: self.splash_box.hide() self.games_scrollwindow.show() self.sidebar_viewport.show() def switch_view(self, view_type): """Switch between grid view and list view.""" logger.debug("Switching view") if view_type == self.get_view_type(): return self.view.destroy() icon_type = self.get_icon_type(view_type) self.game_store.set_icon_type(icon_type) self.view = load_view(view_type, self.game_store) self.view.contextual_menu = self.menu self.connect_signals() self.games_scrollwindow.add(self.view) self.view.show_all() # Note: set_active(True *or* False) apparently makes ALL the menuitems # in the group send the activate signal... if icon_type == "banner_small": self.banner_small_menuitem.set_active(True) if icon_type == "icon": self.icon_menuitem.set_active(True) if icon_type == "banner": self.banner_menuitem.set_active(True) settings.write_setting("view_type", view_type) def sync_library(self): """Synchronize games with local stuff and server.""" def update_gui(result, error): added, updated, installed, uninstalled = result self.switch_splash_screen() self.game_store.fill_store(added) GLib.idle_add(self.update_existing_games, added, updated, installed, uninstalled, True) self.set_status("Syncing library") AsyncCall(Sync().sync_all, update_gui) def update_existing_games(self, added, updated, installed, uninstalled, first_run=False): for game_id in updated.difference(added): self.view.update_row(pga.get_game_by_field(game_id, "id")) for game_id in installed.difference(added): if not self.view.get_row_by_id(game_id): self.view.add_game(game_id) self.view.set_installed(Game(game_id)) for game_id in uninstalled.difference(added): self.view.set_uninstalled(game_id) self.sidebar_treeview.update() if first_run: icons_sync = AsyncCall(self.sync_icons, None, stoppable=True) self.threads_stoppers.append(icons_sync.stop_request.set) self.set_status("Library synced") self.update_runtime() def update_runtime(self): cancellables = runtime.update(self.set_status) self.threads_stoppers += cancellables def sync_icons(self, stop_request=None): resources.fetch_icons( [game for game in pga.get_games()], callback=self.on_image_downloaded, stop_request=stop_request ) def set_status(self, text): self.status_label.set_text(text) def refresh_status(self): """Refresh status bar.""" if self.running_game: name = self.running_game.name if self.running_game.state == self.running_game.STATE_IDLE: self.set_status("Preparing to launch %s" % name) elif self.running_game.state == self.running_game.STATE_STOPPED: self.set_status("Game has quit") display.set_cursor("default", self.window.get_window()) self.stop_button.set_sensitive(False) elif self.running_game.state == self.running_game.STATE_RUNNING: self.set_status("Playing %s" % name) display.set_cursor("default", self.window.get_window()) self.stop_button.set_sensitive(True) for index in range(4): self.joystick_icons.append(self.builder.get_object("js" + str(index) + "image")) if os.path.exists("/dev/input/js%d" % index): self.joystick_icons[index].set_visible(True) else: self.joystick_icons[index].set_visible(False) return True def about(self, _widget, _data=None): """Open the about dialog.""" dialogs.AboutDialog() # Callbacks def on_clear_search(self, widget, icon_pos, event): if icon_pos == Gtk.EntryIconPosition.SECONDARY: widget.set_text("") def on_connect(self, *args): """Callback when a user connects to his account.""" login_dialog = dialogs.ClientLoginDialog() login_dialog.connect("connected", self.on_connect_success) def on_connect_success(self, dialog, credentials): if isinstance(credentials, str): username = credentials else: username = credentials["username"] self.toggle_connection(True, username) self.sync_library() def on_disconnect(self, *args): api.disconnect() self.toggle_connection(False) def toggle_connection(self, is_connected, username=None): disconnect_menuitem = self.builder.get_object("disconnect_menuitem") connect_menuitem = self.builder.get_object("connect_menuitem") connection_label = self.builder.get_object("connection_label") if is_connected: disconnect_menuitem.show() connect_menuitem.hide() connection_status = "Connected as %s" % username else: disconnect_menuitem.hide() connect_menuitem.show() connection_status = "Not connected" logger.info(connection_status) connection_label.set_text(connection_status) def on_register_account(self, *args): Gtk.show_uri(None, "http://lutris.net/user/register", Gdk.CURRENT_TIME) def on_synchronize_manually(self, *args): """Callback when Synchronize Library is activated.""" self.sync_library() def on_sync_timer(self): if not self.running_game or self.running_game.state == Game.STATE_STOPPED: def update_gui(result, error): self.update_existing_games(set(), set(), *result) AsyncCall(Sync().sync_local, update_gui) return True def on_resize(self, widget, *args): self.window_size = widget.get_size() def on_destroy(self, *args): """Signal for window close.""" # Stop cancellable running threads for stopper in self.threads_stoppers: stopper() # Save settings width, height = self.window_size settings.write_setting("width", width) settings.write_setting("height", height) Gtk.main_quit(*args) logger.debug("Quitting lutris") def on_runners_activate(self, _widget, _data=None): """Callback when manage runners is activated.""" RunnersDialog() def on_preferences_activate(self, _widget, _data=None): """Callback when preferences is activated.""" SystemConfigDialog() def on_show_installed_games_toggled(self, widget, data=None): filter_installed = widget.get_active() setting_value = "true" if filter_installed else "false" settings.write_setting("filter_installed", setting_value) self.game_store.filter_installed = filter_installed self.game_store.modelfilter.refilter() def on_pga_menuitem_activate(self, _widget, _data=None): dialogs.PgaSourceDialog() def on_search_entry_changed(self, widget): self.game_store.filter_text = widget.get_text() self.game_store.modelfilter.refilter() def _get_current_game_id(self): """Return the id of the current selected game while taking care of the double clic bug. """ # Wait two seconds to avoid running a game twice if time.time() - self.game_launch_time < 2: return self.game_launch_time = time.time() return self.view.selected_game def on_game_run(self, _widget=None, game_id=None): """Launch a game, or install it if it is not""" if not game_id: game_id = self._get_current_game_id() if not game_id: return display.set_cursor("wait", self.window.get_window()) self.running_game = Game(game_id) if self.running_game.is_installed: self.running_game.play() else: game_slug = self.running_game.slug self.running_game = None InstallerDialog(game_slug, self) def on_game_stop(self, *args): """Stop running game.""" if self.running_game: self.running_game.stop() self.stop_button.set_sensitive(False) def on_install_clicked(self, _widget=None, game_ref=None): """Install a game""" if not game_ref: game_id = self._get_current_game_id() game = pga.get_game_by_field(game_id, "id") game_ref = game.get("slug") logger.debug("Installing game %s (%s)" % (game_ref, game_id)) if not game_ref: return display.set_cursor("wait", self.window.get_window()) InstallerDialog(game_ref, self) def on_keypress(self, widget, event): if event.keyval == Gdk.KEY_F9: self.toggle_sidebar() def game_selection_changed(self, _widget): # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 if type(self.view) is GameGridView: is_double_click = time.time() - self.game_selection_time < 0.4 is_same_game = self.view.selected_game == self.last_selected_game if is_double_click and is_same_game: self.on_game_run() self.game_selection_time = time.time() self.last_selected_game = self.view.selected_game sensitive = True if self.view.selected_game else False self.play_button.set_sensitive(sensitive) self.delete_button.set_sensitive(sensitive) def on_game_installed(self, view, game_id): if not self.view.get_row_by_id(game_id): logger.debug("Adding new installed game to view (%s)" % game_id) self.add_game_to_view(game_id) view.set_installed(Game(game_id)) self.sidebar_treeview.update() def on_image_downloaded(self, game_id): game = Game(game_id) is_installed = game.is_installed self.view.update_image(game_id, is_installed) def add_manually(self, *args): game = Game(self.view.selected_game) add_game_dialog = AddGameDialog(self.window, game) add_game_dialog.run() if add_game_dialog.saved: self.view.set_installed(game) self.sidebar_treeview.update() def on_view_game_log_activate(self, widget): if not self.running_game: dialogs.ErrorDialog("No game log available") return log_title = u"Log for {}".format(self.running_game) log_window = LogWindow(log_title, self.window) log_window.logtextview.set_text(self.running_game.game_log) log_window.run() log_window.destroy() def add_game(self, _widget, _data=None): """Add a new game.""" add_game_dialog = AddGameDialog(self.window) add_game_dialog.run() if add_game_dialog.saved: self.add_game_to_view(add_game_dialog.game.id) def add_game_to_view(self, game_id): if not game_id: raise ValueError("Missing game id") def do_add_game(): self.view.add_game(game_id) self.switch_splash_screen() self.sidebar_treeview.update() GLib.idle_add(do_add_game) def on_remove_game(self, _widget, _data=None): selected_game = self.view.selected_game UninstallGameDialog(game_id=selected_game, callback=self.remove_game_from_view) def remove_game_from_view(self, game_id, from_library=False): def do_remove_game(): self.view.remove_game(game_id) self.switch_splash_screen() if from_library: GLib.idle_add(do_remove_game) else: self.view.update_image(game_id, is_installed=False) self.sidebar_treeview.update() def on_browse_files(self, widget): game = Game(self.view.selected_game) path = game.get_browse_dir() if path and os.path.exists(path): Gtk.show_uri(None, "file://" + path, Gdk.CURRENT_TIME) else: dialogs.NoticeDialog("Can't open %s \nThe folder doesn't exist." % path) def edit_game_configuration(self, _button): """Edit game preferences.""" def on_dialog_saved(): game_id = dialog.game.id self.view.remove_game(game_id) self.view.add_game(game_id) self.view.set_selected_game(game_id) self.sidebar_treeview.update() game = Game(self.view.selected_game) if game.is_installed: dialog = EditGameConfigDialog(self.window, game, on_dialog_saved) def on_viewmenu_toggled(self, menuitem): view_type = "grid" if menuitem.get_active() else "list" if view_type == self.current_view_type: return self.switch_view(view_type) self.grid_view_btn.set_active(view_type == "grid") self.list_view_btn.set_active(view_type == "list") def on_viewbtn_toggled(self, widget): view_type = "grid" if widget.get_active() else "list" if view_type == self.current_view_type: return self.switch_view(view_type) self.grid_view_menuitem.set_active(view_type == "grid") self.list_view_menuitem.set_active(view_type == "list") def on_icon_type_activate(self, menuitem): icon_type = menuitem.get_name() if icon_type == self.game_store.icon_type or not menuitem.get_active(): return if self.current_view_type == "grid": settings.write_setting("icon_type_gridview", icon_type) elif self.current_view_type == "list": settings.write_setting("icon_type_listview", icon_type) self.game_store.set_icon_type(icon_type) def create_menu_shortcut(self, *args): """Add the selected game to the system's Games menu.""" game = Game(self.view.selected_game).name shortcuts.create_launcher(game.slug, game.id, game.name, menu=True) def create_desktop_shortcut(self, *args): """Create a desktop launcher for the selected game.""" game = Game(self.view.selected_game) shortcuts.create_launcher(game.slug, game.id, game.name, desktop=True) def remove_menu_shortcut(self, *args): game = Game(self.view.selected_game) shortcuts.remove_launcher(game.slug, game.id, menu=True) def remove_desktop_shortcut(self, *args): game = Game(self.view.selected_game) shortcuts.remove_launcher(game.slug, game.id, desktop=True) def toggle_sidebar(self): if self.sidebar_viewport.is_visible(): self.sidebar_viewport.hide() else: self.sidebar_viewport.show() def on_sidebar_changed(self, widget): self.view.game_store.filter_runner = widget.get_selected_runner() self.game_store.modelfilter.refilter()
def __init__(self): ui_filename = os.path.join(datapath.get(), 'ui', 'LutrisWindow.ui') if not os.path.exists(ui_filename): raise IOError('File %s not found' % ui_filename) # Currently running game self.running_game = None # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 self.game_selection_time = 0 self.game_launch_time = 0 self.last_selected_game = None self.builder = Gtk.Builder() self.builder.add_from_file(ui_filename) # load config width = int(settings.read_setting('width') or 800) height = int(settings.read_setting('height') or 600) self.window_size = (width, height) view_type = self.get_view_type() self.icon_type = self.get_icon_type(view_type) filter_installed_setting = settings.read_setting( 'filter_installed') or 'false' self.filter_installed = filter_installed_setting == 'true' show_installed_games_menuitem = self.builder.get_object( 'filter_installed') show_installed_games_menuitem.set_active(self.filter_installed) logger.debug("Getting game list") game_list = get_game_list(self.filter_installed) logger.debug("Switching view") self.view = load_view(view_type, game_list, icon_type=self.icon_type) logger.debug("Connecting signals") self.main_box = self.builder.get_object('main_box') self.splash_box = self.builder.get_object('splash_box') # View menu self.grid_view_menuitem = self.builder.get_object("gridview_menuitem") self.grid_view_menuitem.set_active(view_type == 'grid') self.list_view_menuitem = self.builder.get_object("listview_menuitem") self.list_view_menuitem.set_active(view_type == 'list') # View buttons self.grid_view_btn = self.builder.get_object('switch_grid_view_btn') self.grid_view_btn.set_active(view_type == 'grid') self.list_view_btn = self.builder.get_object('switch_list_view_btn') self.list_view_btn.set_active(view_type == 'list') # Icon type menu self.banner_small_menuitem = \ self.builder.get_object('banner_small_menuitem') self.banner_small_menuitem.set_active(self.icon_type == 'banner_small') self.banner_menuitem = self.builder.get_object('banner_menuitem') self.banner_menuitem.set_active(self.icon_type == 'banner') self.icon_menuitem = self.builder.get_object('icon_menuitem') self.icon_menuitem.set_active(self.icon_type == 'grid') self.search_entry = self.builder.get_object('search_entry') # Scroll window self.games_scrollwindow = self.builder.get_object('games_scrollwindow') self.games_scrollwindow.add(self.view) # Status bar self.status_label = self.builder.get_object('status_label') self.joystick_icons = [] # Buttons self.stop_button = self.builder.get_object('stop_button') self.stop_button.set_sensitive(False) self.delete_button = self.builder.get_object('delete_button') self.delete_button.set_sensitive(False) self.play_button = self.builder.get_object('play_button') self.play_button.set_sensitive(False) # Contextual menu menu_callbacks = [ ('play', self.on_game_clicked), ('install', self.on_game_clicked), ('add', self.add_manually), ('configure', self.edit_game_configuration), ('browse', self.on_browse_files), ('desktop-shortcut', self.create_desktop_shortcut), ('menu-shortcut', self.create_menu_shortcut), ('remove', self.on_remove_game), ] self.menu = ContextualMenu(menu_callbacks) self.view.contextual_menu = self.menu # Timer self.timer_id = GLib.timeout_add(2000, self.refresh_status) sidebar_paned = self.builder.get_object('sidebar_paned') sidebar_paned.set_position(150) sidebar_treeview = SidebarTreeView() sidebar_treeview.connect('cursor-changed', self.on_sidebar_changed) self.sidebar_viewport = self.builder.get_object('sidebar_viewport') self.sidebar_viewport.add(sidebar_treeview) # Window initialization self.window = self.builder.get_object("window") self.window.resize_to_geometry(width, height) self.window.show_all() self.builder.connect_signals(self) self.connect_signals() # XXX Hide PGA config menu item until it actually gets implemented pga_menuitem = self.builder.get_object('pga_menuitem') pga_menuitem.hide() self.switch_splash_screen() credentials = api.read_api_key() if credentials: self.on_connect_success(None, credentials) else: self.toggle_connection(False) sync = Sync() async_call(sync.sync_steam_local, lambda r, e: async_call(self.sync_icons, None), caller=self) # Update Lutris Runtime async_call(runtime.update_runtime, None)
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 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.sidebar_width = int(settings.read_setting('sidebar_width') or 180) 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' # 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), ('execute-script', "Execute script", self.on_execute_script_clicked), ('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) self.gui_needs_update = True self.config_menu_first_access = True 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-tray-icon': Action(self.on_tray_icon_toggle, type='b', default=self.show_tray_icon), '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): """Returns which kind of view is currently presented (grid or list)""" return 'grid' if isinstance(self.view, GameGridView) else 'list' def on_steam_game_changed(self, operation, path): """Action taken when a Steam AppManifest file is updated""" 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): """Enables or disbales dark theme""" gtksettings = Gtk.Settings.get_default() gtksettings.set_property("gtk-application-prefer-dark-theme", is_dark) def get_view(self, view_type): """Return the appropriate widget for the current view""" if view_type == 'grid': return GameGridView(self.game_store) 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(): """Return the type of view saved by the user""" 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, force=None): """Toggle the state of the splash screen based on the library contents""" if not self.splash_box.get_visible() and self.game_list: return if self.game_list or force is True: self.splash_box.hide() self.sidebar_paned.show() self.games_scrollwindow.show() else: logger.debug('Showing splash screen') self.splash_box.show() self.sidebar_paned.hide() self.games_scrollwindow.hide() 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 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 error: logger.error("Failed to synchrone library: %s", error) return 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 size = 999 added_games = chain.from_iterable([ pga.get_games_where( id__in=list(added_ids)[page * size:page * size + size]) for page in range(math.ceil(len(added_ids) / size)) ]) self.game_list += added_games self.switch_splash_screen() self.view.populate_games(added_games) 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): """Opens the service sync dialog""" 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 might not work if the game has no 'item' set logger.debug("Updating row for ID %s", game_id) self.view.update_row(pga.get_game_by_field(game_id, 'id')) if first_run: logger.info("Setting up view for first run") for game_id in added: logger.debug("Adding %s", game_id) self.add_game_to_view(game_id) icons_sync = AsyncCall(self.sync_icons, callback=None) self.threads_stoppers.append(icons_sync.stop_request.set) self.set_status("") def update_runtime(self): """Check that the runtime is up to date""" self.runtime_updater.update(self.set_status) self.threads_stoppers += self.runtime_updater.cancellables def sync_icons(self): """Download missing icons""" game_slugs = [game['slug'] for game in self.game_list] if not game_slugs: return logger.debug("Syncing %d icons", len(game_slugs)) try: GLib.idle_add(resources.fetch_icons, game_slugs, 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): """Sets the statusbar text""" # update row at game exit # XXX This is NOT a proper way to do it!!!!!! # FIXME This is ugly and will cause issues!@@! if text == "Game has quit" and self.gui_needs_update: self.view.update_row( pga.get_game_by_field(self.running_game.id, 'id')) 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) self.gui_needs_update = True elif self.running_game.state == self.running_game.STATE_STOPPED: self.set_status("Game has quit") self.gui_needs_update = False 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 self.gui_needs_update = True return True def on_dark_theme_state_change(self, action, value): """Callback for theme switching action""" 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): """Callback for user connect success""" if isinstance(credentials, str): username = credentials else: username = credentials["username"] self.toggle_connection(True, username) self.sync_library() self.connect_link.set_sensitive(False) self.actions['synchronize'].props.enabled = True self.actions['register-account'].props.enabled = False @GtkTemplate.Callback def on_disconnect(self, *_args): """Callback from user disconnect""" api.disconnect() self.toggle_connection(False) self.connect_link.show() self.actions['synchronize'].props.enabled = False def toggle_connection(self, is_connected, username=None): """Sets or unset connected state for the current user""" 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) settings.write_setting('sidebar_width', self.sidebar_width) @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): """Callback to handle installed games first toggle""" 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): """Shows the installed games first in the view""" 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): """Callback to handle uninstalled game filter switch""" action.set_state(value) filter_installed = value.get_boolean() self.set_show_installed_state(filter_installed) def set_show_installed_state(self, filter_installed): """Shows or hide uninstalled games""" 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): """Callback for opening the PGA dialog""" dialogs.PgaSourceDialog(parent=self) @GtkTemplate.Callback def on_search_entry_changed(self, widget): """Callback to handle search entry updates""" 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 None 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 None 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): """Callback to stop a 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): """Callback to handle the selection of a game in the view""" # 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): """Callback to handle newly installed games""" if not isinstance(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): """Callback for handling successful image downloads""" 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): """Callback that presents the Add game dialog""" 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): """Callback for opening the log window""" 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(force=True) 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): """Callback that present the uninstall dialog to the user""" 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): """Remove a game from the view""" 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): """Callback to open a game folder in the file browser""" game = Game(self.view.selected_game) path = game.get_browse_dir() if path and os.path.exists(path): open_uri('file://%s' % path) else: dialogs.NoticeDialog("Can't open %s \nThe folder doesn't exist." % path) def on_view_game(self, _widget): """Callback to open a game on lutris.net""" game = Game(self.view.selected_game) open_uri('https://lutris.net/games/%s' % game.slug) def on_edit_game_configuration(self, _widget): """Edit game preferences""" if self.config_menu_first_access: self.config_menu_first_access = False init_dxvk_versions() 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_execute_script_clicked(self, _widget): """Execute the game's associated script""" game = Game(self.view.selected_game) ondemand_command = game.runner.system_config.get("ondemand_command") if path_exists(ondemand_command): LutrisThread( [ondemand_command], include_processes=[os.path.basename(ondemand_command)], cwd=game.directory).start() logger.info("Running %s in the background", ondemand_command) def on_viewtype_state_change(self, action, val): """Callback to handle view type switch""" 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): """Callback to handle icon size change""" 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): """Remove an XDG menu shortcut""" game = Game(self.view.selected_game) xdg.remove_launcher(game.slug, game.id, menu=True) def remove_desktop_shortcut(self, *_args): """Remove a .desktop shortcut""" game = Game(self.view.selected_game) xdg.remove_launcher(game.slug, game.id, desktop=True) def on_sidebar_state_change(self, action, value): """Callback to handle siderbar toggle""" action.set_state(value) self.sidebar_visible = value.get_boolean() setting = 'true' if self.sidebar_visible else 'false' settings.write_setting('sidebar_visible', setting) self.show_sidebar() def on_tray_icon_toggle(self, action, value): """Callback for handling tray icon toggle""" action.set_state(value) settings.write_setting('show_tray_icon', value) self.application.set_tray_icon(value) def show_sidebar(self): """Displays the sidebar""" width = self.sidebar_width if self.sidebar_visible else 0 self.sidebar_paned.set_position(width) def on_sidebar_changed(self, widget): """Callback to handle selected runner/platforms updates in sidebar""" filer_type, slug = widget.get_selected_filter() selected_runner = None selected_platform = None if not slug: pass elif filer_type == 'platforms': selected_platform = slug elif filer_type == 'runners': selected_runner = slug self.set_selected_filter(selected_runner, selected_platform) def set_selected_filter(self, runner, platform): """Filter the view to a given runner and 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() @GtkTemplate.Callback def on_sidebar_resize(self, widget, *_args): """Size-allocate signal. Updates stored sidebar size. """ self.sidebar_width = widget.get_position()
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, 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.ApplicationWindow): """Handler class for main window signals.""" __gtype_name__ = 'LutrisWindow' main_box = GtkTemplate.Child() splash_box = GtkTemplate.Child() connect_link = GtkTemplate.Child() games_scrollwindow = GtkTemplate.Child() sidebar_paned = GtkTemplate.Child() sidebar_viewport = GtkTemplate.Child() statusbar = GtkTemplate.Child() connection_label = GtkTemplate.Child() status_box = GtkTemplate.Child() def __init__(self, application, **kwargs): self.runtime_updater = RuntimeUpdater() self.running_game = None self.threads_stoppers = [] # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 self.game_selection_time = 0 self.game_launch_time = 0 self.last_selected_game = None self.selected_runner = None self.selected_platform = None # Load settings width = int(settings.read_setting('width') or 800) height = int(settings.read_setting('height') or 600) self.window_size = (width, height) self.maximized = settings.read_setting('maximized') == 'True' view_type = self.get_view_type() self.load_icon_type_from_settings(view_type) self.filter_installed = \ settings.read_setting('filter_installed') == 'true' self.sidebar_visible = \ settings.read_setting('sidebar_visible') in ['true', None] self.use_dark_theme = settings.read_setting('dark_theme') == 'true' # Sync local lutris library with current Steam games and desktop games for service in get_services_synced_at_startup(): service.sync_with_lutris() # Window initialization self.game_list = pga.get_games() self.game_store = GameStore([], self.icon_type, self.filter_installed) self.view = self.get_view(view_type) super().__init__(default_width=width, default_height=height, icon_name='lutris', application=application, **kwargs) if self.maximized: self.maximize() self.init_template() self._init_actions() # Set theme to dark if set in the settings self.set_dark_theme(self.use_dark_theme) # Load view self.games_scrollwindow.add(self.view) self.connect_signals() self.view.show() # Contextual menu main_entries = [ ('play', "Play", self.on_game_run), ('install', "Install", self.on_install_clicked), ('add', "Add manually", self.on_add_manually), ('configure', "Configure", self.on_edit_game_configuration), ('browse', "Browse files", self.on_browse_files), ('desktop-shortcut', "Create desktop shortcut", self.create_desktop_shortcut), ('rm-desktop-shortcut', "Delete desktop shortcut", self.remove_desktop_shortcut), ('menu-shortcut', "Create application menu shortcut", self.create_menu_shortcut), ('rm-menu-shortcut', "Delete application menu shortcut", self.remove_menu_shortcut), ('install_more', "Install (add) another version", self.on_install_clicked), ('remove', "Remove", self.on_remove_game), ('view', "View on Lutris.net", self.on_view_game), ] self.menu = ContextualMenu(main_entries) self.view.contextual_menu = self.menu # Sidebar self.sidebar_treeview = SidebarTreeView() self.sidebar_treeview.connect('cursor-changed', self.on_sidebar_changed) self.sidebar_viewport.add(self.sidebar_treeview) self.sidebar_treeview.show() self.game_store.fill_store(self.game_list) self.switch_splash_screen() self.show_sidebar() self.update_runtime() # Connect account and/or sync credentials = api.read_api_key() if credentials: self.on_connect_success(None, credentials) else: self.toggle_connection(False) self.sync_library() # Timers self.timer_ids = [GLib.timeout_add(300, self.refresh_status)] steamapps_paths = steam.get_steamapps_paths(flat=True) self.steam_watcher = SteamWatcher(steamapps_paths, self.on_steam_game_changed) def _init_actions(self): Action = namedtuple('Action', ('callback', 'type', 'enabled', 'default', 'accel')) Action.__new__.__defaults__ = (None, None, True, None, None) actions = { 'browse-games': Action( lambda *x: 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()), 'sync-local': Action(lambda *x: self.open_sync_dialog()), 'add-game': Action(self.on_add_game_button_clicked), 'view-game-log': Action(self.on_view_game_log_activate), 'stop-game': Action(self.on_game_stop, enabled=False), 'start-game': Action(self.on_game_run, enabled=False), 'remove-game': Action(self.on_remove_game, enabled=False), 'preferences': Action(self.on_preferences_activate), 'manage-runners': Action(lambda *x: RunnersDialog()), 'about': Action(self.on_about_clicked), 'show-installed-only': Action(self.on_show_installed_state_change, type='b', default=self.filter_installed, accel='<Primary>h'), 'view-type': Action(self.on_viewtype_state_change, type='s', default=self.current_view_type), 'icon-type': Action(self.on_icontype_state_change, type='s', default=self.icon_type), 'use-dark-theme': Action(self.on_dark_theme_state_change, type='b', default=self.use_dark_theme), 'show-side-bar': Action(self.on_sidebar_state_change, type='b', default=self.sidebar_visible, accel='F9'), } self.actions = {} app = self.props.application for name, value in actions.items(): if not value.type: action = Gio.SimpleAction.new(name) action.connect('activate', value.callback) else: default_value = None param_type = None if value.default is not None: default_value = GLib.Variant(value.type, value.default) if value.type != 'b': param_type = default_value.get_type() action = Gio.SimpleAction.new_stateful(name, param_type, default_value) action.connect('change-state', value.callback) self.actions[name] = action if value.enabled is False: action.props.enabled = False self.add_action(action) if value.accel: app.add_accelerator(value.accel, 'win.' + name) @property def current_view_type(self): return 'grid' if isinstance(self.view, GameGridView) else 'list' def on_steam_game_changed(self, operation, path): appmanifest = steam.AppManifest(path) runner_name = appmanifest.get_runner_name() games = pga.get_games_where(steamid=appmanifest.steamid) if operation == Gio.FileMonitorEvent.DELETED: for game in games: if game['runner'] == runner_name: steam.mark_as_uninstalled(game) self.view.set_uninstalled(Game(game['id'])) break elif operation in (Gio.FileMonitorEvent.CHANGED, Gio.FileMonitorEvent.CREATED): if not appmanifest.is_installed(): return if runner_name == 'winesteam': return game_info = None for game in games: if game['installed'] == 0: game_info = game else: # Game is already installed, don't do anything return if not game_info: game_info = { 'name': appmanifest.name, 'slug': appmanifest.slug, } game_id = steam.mark_as_installed(appmanifest.steamid, runner_name, game_info) game_ids = [game['id'] for game in self.game_list] if game_id not in game_ids: self.add_game_to_view(game_id) else: self.view.set_installed(Game(game_id)) @staticmethod def set_dark_theme(is_dark): gtksettings = Gtk.Settings.get_default() gtksettings.set_property("gtk-application-prefer-dark-theme", is_dark) def get_view(self, view_type): if view_type == 'grid': return GameGridView(self.game_store) else: return GameListView(self.game_store) def connect_signals(self): """Connect signals from the view with the main window. This must be called each time the view is rebuilt. """ self.view.connect('game-installed', self.on_game_installed) self.view.connect("game-activated", self.on_game_run) self.view.connect("game-selected", self.game_selection_changed) self.view.connect("remove-game", self.on_remove_game) @staticmethod def check_update(): """Verify availability of client update.""" version_request = http.Request('https://lutris.net/version') version_request.get() version = version_request.content if version: latest_version = settings.read_setting('latest_version') if version > (latest_version or settings.VERSION): dialogs.ClientUpdateDialog() # Store latest version seen to avoid showing # the dialog more than once. settings.write_setting('latest_version', version) @staticmethod def get_view_type(): view_type = settings.read_setting('view_type') if view_type in ['grid', 'list']: return view_type return settings.GAME_VIEW def load_icon_type_from_settings(self, view_type): """Return the icon style depending on the type of view.""" if view_type == 'list': self.icon_type = settings.read_setting('icon_type_listview') default = settings.ICON_TYPE_LISTVIEW else: self.icon_type = settings.read_setting('icon_type_gridview') default = settings.ICON_TYPE_GRIDVIEW if self.icon_type not in ("banner_small", "banner", "icon", "icon_small"): self.icon_type = default return self.icon_type def switch_splash_screen(self): if len(self.game_list) == 0: self.splash_box.show() self.sidebar_paned.hide() self.games_scrollwindow.hide() else: self.splash_box.hide() self.sidebar_paned.show() self.games_scrollwindow.show() def switch_view(self, view_type): """Switch between grid view and list view.""" self.view.destroy() self.load_icon_type_from_settings(view_type) self.game_store.set_icon_type(self.icon_type) self.view = self.get_view(view_type) self.view.contextual_menu = self.menu self.connect_signals() scrollwindow_children = self.games_scrollwindow.get_children() if len(scrollwindow_children): child = scrollwindow_children[0] child.destroy() self.games_scrollwindow.add(self.view) self.set_selected_filter(self.selected_runner, self.selected_platform) self.set_show_installed_state(self.filter_installed) self.view.show_all() settings.write_setting('view_type', view_type) def sync_library(self): """Synchronize games with local stuff and server.""" def update_gui(result, error): if result: added_ids, updated_ids = result # sqlite limits the number of query parameters to 999, to # bypass that limitation, divide the query in chunks page_size = 999 added_games = chain.from_iterable([ pga.get_games_where(id__in=list(added_ids)[p * page_size:p * page_size + page_size]) for p in range(math.ceil(len(added_ids) / page_size)) ]) self.game_list += added_games self.view.populate_games(added_games) self.switch_splash_screen() GLib.idle_add(self.update_existing_games, added_ids, updated_ids, True) else: logger.error("No results returned when syncing the library") self.set_status("Syncing library") AsyncCall(sync_from_remote, update_gui) def open_sync_dialog(self): sync_dialog = SyncServiceDialog(parent=self) sync_dialog.run() def update_existing_games(self, added, updated, first_run=False): for game_id in updated.difference(added): # XXX this migth not work if the game has no 'item' set self.view.update_row(pga.get_game_by_field(game_id, 'id')) if first_run: icons_sync = AsyncCall(self.sync_icons, callback=None) self.threads_stoppers.append(icons_sync.stop_request.set) self.set_status("") def update_runtime(self): self.runtime_updater.update(self.set_status) self.threads_stoppers += self.runtime_updater.cancellables def sync_icons(self): resources.fetch_icons([game['slug'] for game in self.game_list], callback=self.on_image_downloaded) def set_status(self, text): for child_widget in self.status_box.get_children(): child_widget.destroy() label = Gtk.Label(text) label.show() self.status_box.add(label) def refresh_status(self): """Refresh status bar.""" if self.running_game: name = self.running_game.name if self.running_game.state == self.running_game.STATE_IDLE: self.set_status("Preparing to launch %s" % name) elif self.running_game.state == self.running_game.STATE_STOPPED: self.set_status("Game has quit") self.actions['stop-game'].props.enabled = False elif self.running_game.state == self.running_game.STATE_RUNNING: self.set_status("Playing %s" % name) self.actions['stop-game'].props.enabled = True return True # --------- # Callbacks # --------- def on_dark_theme_state_change(self, action, value): action.set_state(value) self.use_dark_theme = value.get_boolean() setting_value = 'true' if self.use_dark_theme else 'false' settings.write_setting('dark_theme', setting_value) self.set_dark_theme(self.use_dark_theme) @GtkTemplate.Callback def on_connect(self, *args): """Callback when a user connects to his account.""" login_dialog = dialogs.ClientLoginDialog(self) login_dialog.connect('connected', self.on_connect_success) return True def on_connect_success(self, dialog, credentials): if isinstance(credentials, str): username = credentials else: username = credentials["username"] self.toggle_connection(True, username) self.sync_library() self.connect_link.hide() self.actions['synchronize'].props.enabled = True @GtkTemplate.Callback def on_disconnect(self, *args): api.disconnect() self.toggle_connection(False) self.connect_link.show() self.actions['synchronize'].props.enabled = False def toggle_connection(self, is_connected, username=None): self.props.application.set_connect_state(is_connected) if is_connected: connection_status = username logger.info('Connected to lutris.net as %s', connection_status) else: connection_status = "Not connected" self.connection_label.set_text(connection_status) @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""" installer_desc = game_slug if game_slug else installer_file if revision: installer_desc += " (%s)" % revision logger.info("Installing %s" % installer_desc) if not game_slug and not installer_file: # Install the currently selected game in the UI game_id = self._get_current_game_id() game = pga.get_game_by_field(game_id, 'id') game_slug = game.get('slug') if not game_slug and not installer_file: return InstallerDialog(game_slug=game_slug, installer_file=installer_file, revision=revision, parent=self) def game_selection_changed(self, _widget): # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 if isinstance(self.view, GameGridView): is_double_click = time.time() - self.game_selection_time < 0.4 is_same_game = self.view.selected_game == self.last_selected_game if is_double_click and is_same_game: self.on_game_run() self.game_selection_time = time.time() self.last_selected_game = self.view.selected_game sensitive = True if self.view.selected_game else False self.actions['start-game'].props.enabled = sensitive self.actions['remove-game'].props.enabled = sensitive def on_game_installed(self, view, game_id): if type(game_id) != int: raise ValueError("game_id must be an int") if not self.view.has_game_id(game_id): logger.debug("Adding new installed game to view (%d)" % game_id) self.add_game_to_view(game_id, async=False) game = Game(game_id) view.set_installed(game) self.sidebar_treeview.update() GLib.idle_add(resources.fetch_icons, [game.slug], self.on_image_downloaded) def on_image_downloaded(self, game_slugs): for game_slug in game_slugs: games = pga.get_games_where(slug=game_slug) for game in games: game = Game(game['id']) is_installed = game.is_installed self.view.update_image(game.id, is_installed) def on_add_manually(self, widget, *args): def on_game_added(game): self.view.set_installed(game) self.sidebar_treeview.update() game = Game(self.view.selected_game) AddGameDialog(self, game=game, runner=self.selected_runner, callback=lambda: on_game_added(game)) @GtkTemplate.Callback def on_view_game_log_activate(self, *args): if not self.running_game: dialogs.ErrorDialog('No game log available', parent=self) return log_title = u"Log for {}".format(self.running_game) log_window = LogWindow(title=log_title, buffer=self.running_game.log_buffer, parent=self) log_window.run() log_window.destroy() @GtkTemplate.Callback def on_add_game_button_clicked(self, *args): """Add a new game manually with the AddGameDialog.""" dialog = AddGameDialog( self, runner=self.selected_runner, callback=lambda: self.add_game_to_view(dialog.game.id) ) return True def add_game_to_view(self, game_id, async=True): if not game_id: raise ValueError("Missing game id") def do_add_game(): self.view.add_game_by_id(game_id) self.switch_splash_screen() self.sidebar_treeview.update() return False if async: GLib.idle_add(do_add_game) else: do_add_game()
class LutrisWindow(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, 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)
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()
class LutrisWindow(object): """Handler class for main window signals.""" def __init__(self, service=None): ui_filename = os.path.join( datapath.get(), 'ui', 'LutrisWindow.ui' ) if not os.path.exists(ui_filename): raise IOError('File %s not found' % ui_filename) self.service = service self.running_game = None self.threads_stoppers = [] # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 self.game_selection_time = 0 self.game_launch_time = 0 self.last_selected_game = None self.builder = Gtk.Builder() self.builder.add_from_file(ui_filename) # Load settings width = int(settings.read_setting('width') or 800) height = int(settings.read_setting('height') or 600) self.window_size = (width, height) view_type = self.get_view_type() self.icon_type = self.get_icon_type(view_type) filter_installed = \ settings.read_setting('filter_installed') == 'true' self.sidebar_visible = \ settings.read_setting('sidebar_visible') in ['true', None] # Load view logger.debug("Loading view") self.game_store = GameStore([], self.icon_type, filter_installed) self.view = load_view(view_type, self.game_store) logger.debug("Connecting signals") self.main_box = self.builder.get_object('main_box') self.splash_box = self.builder.get_object('splash_box') # View menu installed_games_only_menuitem =\ self.builder.get_object('filter_installed') installed_games_only_menuitem.set_active(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) # Status bar self.status_label = self.builder.get_object('status_label') self.joystick_icons = [] # Buttons self.stop_button = self.builder.get_object('stop_button') self.stop_button.set_sensitive(False) self.delete_button = self.builder.get_object('delete_button') self.delete_button.set_sensitive(False) self.play_button = self.builder.get_object('play_button') self.play_button.set_sensitive(False) # Contextual menu main_entries = [ ('play', "Play", self.on_game_run), ('install', "Install", self.on_install_clicked), ('add', "Add manually", self.add_manually), ('configure', "Configure", self.edit_game_configuration), ('browse', "Browse files", self.on_browse_files), ('desktop-shortcut', "Create desktop shortcut", self.create_desktop_shortcut), ('rm-desktop-shortcut', "Delete desktop shortcut", self.remove_desktop_shortcut), ('menu-shortcut', "Create application menu shortcut", self.create_menu_shortcut), ('rm-menu-shortcut', "Delete application menu shortcut", self.remove_menu_shortcut), ('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 sidebar_paned = self.builder.get_object('sidebar_paned') sidebar_paned.set_position(150) self.sidebar_treeview = SidebarTreeView() self.sidebar_treeview.connect('cursor-changed', self.on_sidebar_changed) self.sidebar_viewport = self.builder.get_object('sidebar_viewport') self.sidebar_viewport.add(self.sidebar_treeview) # Window initialization self.window = self.builder.get_object("window") self.window.resize_to_geometry(width, height) self.window.show_all() if not self.sidebar_visible: self.sidebar_viewport.hide() self.builder.connect_signals(self) self.connect_signals() # XXX Hide PGA config menu item until it actually gets implemented pga_menuitem = self.builder.get_object('pga_menuitem') pga_menuitem.hide() self.init_game_store() 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), GLib.timeout_add(10000, self.on_sync_timer)] def init_game_store(self): logger.debug("Getting game list") game_list = get_game_list() self.game_store.fill_store(game_list) self.switch_splash_screen() @property def current_view_type(self): return 'grid' \ if self.view.__class__.__name__ == "GameGridView" \ else 'list' def connect_signals(self): """Connect signals from the view with the main window. This must be called each time the view is rebuilt. """ self.view.connect('game-installed', self.on_game_installed) self.view.connect("game-activated", self.on_game_run) self.view.connect("game-selected", self.game_selection_changed) self.window.connect("configure-event", self.on_resize) def check_update(self): """Verify availability of client update.""" pass def on_version_received(version, error): if not version: return 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) import http # Move me AsyncCall(http.download_content, on_version_received, 'https://lutris.net/version') def get_view_type(self): view_type = settings.read_setting('view_type') if view_type in ['grid', 'list']: return view_type return settings.GAME_VIEW def get_icon_type(self, view_type): """Return the icon style depending on the type of view.""" if view_type == 'list': icon_type = settings.read_setting('icon_type_listview') default = settings.ICON_TYPE_LISTVIEW else: icon_type = settings.read_setting('icon_type_gridview') default = settings.ICON_TYPE_GRIDVIEW if icon_type not in ("banner_small", "banner", "icon"): icon_type = default return icon_type def switch_splash_screen(self): if not pga.get_table_length(): self.splash_box.show() self.games_scrollwindow.hide() self.sidebar_viewport.hide() else: self.splash_box.hide() self.games_scrollwindow.show() if self.sidebar_visible: self.sidebar_viewport.show() def switch_view(self, view_type): """Switch between grid view and list view.""" logger.debug("Switching view") if view_type == self.get_view_type(): return self.view.destroy() icon_type = self.get_icon_type(view_type) self.game_store.set_icon_type(icon_type) self.view = load_view(view_type, self.game_store) self.view.contextual_menu = self.menu self.connect_signals() self.games_scrollwindow.add(self.view) self.view.show_all() # Note: set_active(True *or* False) apparently makes ALL the menuitems # in the group send the activate signal... if icon_type == 'banner_small': self.banner_small_menuitem.set_active(True) if icon_type == 'icon': self.icon_menuitem.set_active(True) if icon_type == 'banner': self.banner_menuitem.set_active(True) settings.write_setting('view_type', view_type) def sync_library(self): """Synchronize games with local stuff and server.""" def update_gui(result, error): if result: added, updated, installed, uninstalled = result self.switch_splash_screen() self.game_store.fill_store(added) GLib.idle_add(self.update_existing_games, added, updated, installed, uninstalled, True) else: logger.error("No results returned when syncing the library") self.set_status("Syncing library") AsyncCall(Sync().sync_all, update_gui) def update_existing_games(self, added, updated, installed, uninstalled, first_run=False): for game_id in updated.difference(added): self.view.update_row(pga.get_game_by_field(game_id, 'id')) for game_id in installed.difference(added): if not self.view.get_row_by_id(game_id): self.view.add_game(game_id) self.view.set_installed(Game(game_id)) for game_id in uninstalled.difference(added): self.view.set_uninstalled(game_id) self.sidebar_treeview.update() if first_run: icons_sync = AsyncCall(self.sync_icons, None, stoppable=True) self.threads_stoppers.append(icons_sync.stop_request.set) self.set_status("Library synced") def update_runtime(self): cancellables = runtime.update(self.set_status) self.threads_stoppers += cancellables def sync_icons(self, stop_request=None): resources.fetch_icons([game for game in pga.get_games()], callback=self.on_image_downloaded, stop_request=stop_request) def set_status(self, text): self.status_label.set_text(text) def refresh_status(self): """Refresh status bar.""" if self.running_game: name = self.running_game.name if self.running_game.state == self.running_game.STATE_IDLE: self.set_status("Preparing to launch %s" % name) elif self.running_game.state == self.running_game.STATE_STOPPED: self.set_status("Game has quit") display.set_cursor('default', self.window.get_window()) self.stop_button.set_sensitive(False) elif self.running_game.state == self.running_game.STATE_RUNNING: self.set_status("Playing %s" % name) display.set_cursor('default', self.window.get_window()) self.stop_button.set_sensitive(True) for index in range(4): self.joystick_icons.append( self.builder.get_object('js' + str(index) + 'image') ) if os.path.exists("/dev/input/js%d" % index): self.joystick_icons[index].set_visible(True) else: self.joystick_icons[index].set_visible(False) return True def about(self, _widget, _data=None): """Open the about dialog.""" dialogs.AboutDialog(parent=self.window) # --------- # Callbacks # --------- def on_clear_search(self, widget, icon_pos, event): if icon_pos == Gtk.EntryIconPosition.SECONDARY: widget.set_text('') def on_connect(self, *args): """Callback when a user connects to his account.""" login_dialog = dialogs.ClientLoginDialog(self.window) login_dialog.connect('connected', self.on_connect_success) def on_connect_success(self, dialog, credentials): if isinstance(credentials, str): username = credentials else: username = credentials["username"] self.toggle_connection(True, username) self.sync_library() def on_disconnect(self, *args): api.disconnect() self.toggle_connection(False) def toggle_connection(self, is_connected, username=None): disconnect_menuitem = self.builder.get_object('disconnect_menuitem') connect_menuitem = self.builder.get_object('connect_menuitem') connection_label = self.builder.get_object('connection_label') if is_connected: disconnect_menuitem.show() connect_menuitem.hide() connection_status = "Connected as %s" % username else: disconnect_menuitem.hide() connect_menuitem.show() connection_status = "Not connected" logger.info(connection_status) connection_label.set_text(connection_status) def on_register_account(self, *args): Gtk.show_uri(None, "http://lutris.net/user/register", Gdk.CURRENT_TIME) def on_synchronize_manually(self, *args): """Callback when Synchronize Library is activated.""" self.sync_library() def on_sync_timer(self): if (not self.running_game or self.running_game.state == Game.STATE_STOPPED): def update_gui(result, error): if result: self.update_existing_games(set(), set(), *result) else: logger.error('No results while syncing local Steam database') AsyncCall(Sync().sync_local, update_gui) return True def on_resize(self, widget, *args): self.window_size = widget.get_size() def on_destroy(self, *args): """Signal for window close.""" # Stop cancellable running threads for stopper in self.threads_stoppers: stopper() if self.running_game: 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) logger.debug("Quitting lutris") def on_runners_activate(self, _widget, _data=None): """Callback when manage runners is activated.""" RunnersDialog() def on_preferences_activate(self, _widget, _data=None): """Callback when preferences is activated.""" SystemConfigDialog() def on_show_installed_games_toggled(self, widget, data=None): filter_installed = widget.get_active() setting_value = 'true' if filter_installed else 'false' settings.write_setting( 'filter_installed', setting_value ) self.game_store.filter_installed = filter_installed self.game_store.modelfilter.refilter() def on_pga_menuitem_activate(self, _widget, _data=None): dialogs.PgaSourceDialog(parent=self.window) def on_search_entry_changed(self, widget): self.game_store.filter_text = widget.get_text() self.game_store.modelfilter.refilter() def _get_current_game_id(self): """Return the id of the current selected game while taking care of the double clic bug. """ # Wait two seconds to avoid running a game twice if time.time() - self.game_launch_time < 2: return self.game_launch_time = time.time() return self.view.selected_game def on_game_run(self, _widget=None, game_id=None): """Launch a game, or install it if it is not""" if not game_id: game_id = self._get_current_game_id() if not game_id: return display.set_cursor('wait', self.window.get_window()) self.running_game = Game(game_id) if self.running_game.is_installed: self.running_game.play() else: game_slug = self.running_game.slug self.running_game = None InstallerDialog(game_slug, self) def on_game_stop(self, *args): """Stop running game.""" if self.running_game: self.running_game.stop() self.stop_button.set_sensitive(False) def on_install_clicked(self, _widget=None, game_ref=None): """Install a game""" if not game_ref: game_id = self._get_current_game_id() game = pga.get_game_by_field(game_id, 'id') game_ref = game.get('slug') logger.debug("Installing game %s (%s)" % (game_ref, game_id)) if not game_ref: return display.set_cursor('wait', self.window.get_window()) InstallerDialog(game_ref, self) def 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.get_row_by_id(game_id): logger.debug("Adding new installed game to view (%d)" % game_id) self.add_game_to_view(game_id, async=False) view.set_installed(Game(game_id)) self.sidebar_treeview.update() game_data = pga.get_game_by_field(game_id, field='id') GLib.idle_add(resources.fetch_icons, [game_data], self.on_image_downloaded) def on_image_downloaded(self, game_id): game = Game(game_id) is_installed = game.is_installed self.view.update_image(game_id, is_installed) def add_manually(self, *args): def on_game_added(game): self.view.set_installed(game) self.sidebar_treeview.update() game = Game(self.view.selected_game) AddGameDialog(self.window, game, 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 add_game(self, _widget, _data=None): """Add a new game.""" dialog = AddGameDialog( self.window, callback=lambda: self.add_game_to_view(dialog.game.id) ) 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(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): ui_filename = os.path.join( datapath.get(), 'ui', 'LutrisWindow.ui' ) if not os.path.exists(ui_filename): raise IOError('File %s not found' % ui_filename) # Currently running game self.running_game = None # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 self.game_selection_time = 0 self.game_launch_time = 0 self.last_selected_game = None self.builder = Gtk.Builder() self.builder.add_from_file(ui_filename) # load config width = int(settings.read_setting('width') or 800) height = int(settings.read_setting('height') or 600) self.window_size = (width, height) view_type = self.get_view_type() self.icon_type = self.get_icon_type(view_type) filter_installed_setting = settings.read_setting( 'filter_installed' ) or 'false' self.filter_installed = filter_installed_setting == 'true' show_installed_games_menuitem = self.builder.get_object( 'filter_installed' ) show_installed_games_menuitem.set_active(self.filter_installed) logger.debug("Getting game list") game_list = get_game_list(self.filter_installed) logger.debug("Switching view") self.view = load_view(view_type, game_list, icon_type=self.icon_type) logger.debug("Connecting signals") self.main_box = self.builder.get_object('main_box') self.splash_box = self.builder.get_object('splash_box') # View menu self.grid_view_menuitem = self.builder.get_object("gridview_menuitem") self.grid_view_menuitem.set_active(view_type == 'grid') self.list_view_menuitem = self.builder.get_object("listview_menuitem") self.list_view_menuitem.set_active(view_type == 'list') # View buttons self.grid_view_btn = self.builder.get_object('switch_grid_view_btn') self.grid_view_btn.set_active(view_type == 'grid') self.list_view_btn = self.builder.get_object('switch_list_view_btn') self.list_view_btn.set_active(view_type == 'list') # Icon type menu self.banner_small_menuitem = \ self.builder.get_object('banner_small_menuitem') self.banner_small_menuitem.set_active(self.icon_type == 'banner_small') self.banner_menuitem = self.builder.get_object('banner_menuitem') self.banner_menuitem.set_active(self.icon_type == 'banner') self.icon_menuitem = self.builder.get_object('icon_menuitem') self.icon_menuitem.set_active(self.icon_type == 'icon') self.search_entry = self.builder.get_object('search_entry') self.search_entry.connect('icon-press', self.on_clear_search) # Scroll window self.games_scrollwindow = self.builder.get_object('games_scrollwindow') self.games_scrollwindow.add(self.view) # Status bar self.status_label = self.builder.get_object('status_label') self.joystick_icons = [] # Buttons self.stop_button = self.builder.get_object('stop_button') self.stop_button.set_sensitive(False) self.delete_button = self.builder.get_object('delete_button') self.delete_button.set_sensitive(False) self.play_button = self.builder.get_object('play_button') self.play_button.set_sensitive(False) # Contextual menu menu_callbacks = [ ('play', self.on_game_clicked), ('install', self.on_install_clicked), ('add', self.add_manually), ('configure', self.edit_game_configuration), ('browse', self.on_browse_files), ('desktop-shortcut', self.create_desktop_shortcut), ('menu-shortcut', self.create_menu_shortcut), ('remove', self.on_remove_game), ] self.menu = ContextualMenu(menu_callbacks) self.view.contextual_menu = self.menu # Timer self.timer_id = GLib.timeout_add(2000, self.refresh_status) sidebar_paned = self.builder.get_object('sidebar_paned') sidebar_paned.set_position(150) sidebar_treeview = SidebarTreeView() sidebar_treeview.connect('cursor-changed', self.on_sidebar_changed) self.sidebar_viewport = self.builder.get_object('sidebar_viewport') self.sidebar_viewport.add(sidebar_treeview) # Window initialization self.window = self.builder.get_object("window") self.window.resize_to_geometry(width, height) self.window.show_all() self.builder.connect_signals(self) self.connect_signals() # XXX Hide PGA config menu item until it actually gets implemented pga_menuitem = self.builder.get_object('pga_menuitem') pga_menuitem.hide() self.switch_splash_screen() # Connect account and/or sync credentials = api.read_api_key() if credentials: self.on_connect_success(None, credentials) else: self.toggle_connection(False) sync = Sync() async_call( sync.sync_steam_local, lambda r, e: async_call(self.sync_icons, None), caller=self ) # Update Runtime async_call(runtime.update_runtime, None, self.set_status)