Esempio n. 1
0
    def popup_contextual_menu(self, view, event):
        """Contextual menu."""
        if event.button != Gdk.BUTTON_SECONDARY:
            return
        view.current_path = view.get_path_at_pos(event.x, event.y)
        if view.current_path:
            view.select()
            _iter = self.get_model().get_iter(view.current_path[0])
            if not _iter:
                return
            selected_id = self.get_selected_id(_iter)
            game_row = self.game_store.get_row_by_id(selected_id)
            game_id = None
            if self.service:
                game = get_game_for_service(self.service, game_row[COL_ID])
                if game:
                    game_id = game["id"]
            else:
                game_id = game_row[COL_ID]
            if not game_id:
                return
            game = Game(game_id)
            game_actions = GameActions()
            game_actions.set_game(game=game)

            self.contextual_menu.popup(event, game_actions)
Esempio n. 2
0
    def popup(self, event, game_row=None, game=None):
        if game_row:
            game = Game(game_row[COL_ID])

        if not game:
            logger.error("No game provided, can't open pop-up menu")
            return

        # Clear existing menu
        for item in self.get_children():
            self.remove(item)

        # Main items
        self.add_menuitems(self.main_entries)
        # Runner specific items
        if game.runner_name and game.is_installed:
            runner_entries = self.get_runner_entries(game)
            if runner_entries:
                self.append(Gtk.SeparatorMenuItem())
                self.add_menuitems(runner_entries)
        self.show_all()

        game_actions = GameActions()
        game_actions.set_game(game=game)

        displayed = game_actions.get_displayed_entries()
        disabled_entries = game_actions.get_disabled_entries()
        for menuitem in self.get_children():
            if not isinstance(menuitem, Gtk.ImageMenuItem):
                continue
            menuitem.set_visible(displayed.get(menuitem.action_id, True))
            menuitem.set_sensitive(
                not disabled_entries.get(menuitem.action_id))

        super().popup(None, None, None, None, event.button, event.time)
Esempio n. 3
0
    def popup(self, event, game_row=None, game=None):
        if game_row:
            # FIXME a new game instance is created here, without taking into
            # account running games.
            game = Game(game_row[COL_ID])

        if not game:
            logger.error("No game provided, can't open pop-up menu")
            return

        # Clear existing menu
        for item in self.get_children():
            self.remove(item)

        # Main items
        for entry in self.main_entries:
            self.add_menuitem(entry)

        # Runner specific items
        if game.runner_name and game.is_installed:
            runner_entries = self.get_runner_entries(game)
            if runner_entries:
                self.append(Gtk.SeparatorMenuItem())
                for entry in runner_entries:
                    self.add_menuitem(entry)
        self.show_all()

        game_actions = GameActions()
        game_actions.set_game(game=game)

        displayed = game_actions.get_displayed_entries()
        for menuitem in self.get_children():
            if not isinstance(menuitem, Gtk.ImageMenuItem):
                continue
            menuitem.set_visible(displayed.get(menuitem.action_id, True))

        super().popup(None, None, None, None, event.button, event.time)
Esempio n. 4
0
    def __init__(self, application, **kwargs):
        width = int(settings.read_setting("width") or self.default_width)
        height = int(settings.read_setting("height") or self.default_height)
        super().__init__(default_width=width,
                         default_height=height,
                         icon_name="lutris",
                         application=application,
                         **kwargs)
        self.application = application
        self.runtime_updater = RuntimeUpdater()
        self.threads_stoppers = []
        self.selected_runner = None
        self.selected_platform = None
        self.icon_type = None

        # Load settings
        self.window_size = (width, height)
        self.maximized = settings.read_setting("maximized") == "True"

        view_type = self.get_view_type()
        self.load_icon_type_from_settings(view_type)

        # Window initialization
        self.game_actions = GameActions(application=application, window=self)

        self.search_terms = None
        self.search_timer_id = None
        self.search_mode = "local"
        self.game_store = self.get_store()
        self.view = self.get_view(view_type)

        GObject.add_emission_hook(Game, "game-updated", self.on_game_updated)
        GObject.add_emission_hook(Game, "game-removed", self.on_game_updated)
        GObject.add_emission_hook(Game, "game-installed",
                                  self.on_game_installed)
        GObject.add_emission_hook(GenericPanel, "running-game-selected",
                                  self.game_selection_changed)
        self.connect("delete-event", self.on_window_delete)
        if self.maximized:
            self.maximize()
        self.init_template()
        self._init_actions()
        self._bind_zoom_adjustment()

        # Load view
        self.games_scrollwindow.add(self.view)
        self._connect_signals()
        # Set theme to dark if set in the settings
        self.set_dark_theme()
        self.set_viewtype_icon(view_type)

        # Add additional widgets
        lutris_icon = Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU)
        lutris_icon.set_margin_right(10)
        lutris_icon.set_margin_left(10)
        self.website_search_toggle.set_image(lutris_icon)
        self.website_search_toggle.set_tooltip_text("Search on Lutris.net")
        self.sidebar_listbox = SidebarListBox()
        self.sidebar_listbox.set_size_request(250, -1)
        self.sidebar_listbox.connect("selected-rows-changed",
                                     self.on_sidebar_changed)
        self.sidebar_scrolled.add(self.sidebar_listbox)

        self.game_panel = GenericPanel(application=self.application)

        self.game_scrolled = Gtk.ScrolledWindow(visible=True)
        self.game_scrolled.set_size_request(320, -1)
        self.game_scrolled.get_style_context().add_class("game-scrolled")
        self.game_scrolled.set_policy(Gtk.PolicyType.EXTERNAL,
                                      Gtk.PolicyType.EXTERNAL)
        self.game_scrolled.add(self.game_panel)

        self.panel_revealer = Gtk.Revealer(visible=True)
        self.panel_revealer.set_transition_duration(300)
        self.panel_revealer.set_transition_type(
            Gtk.RevealerTransitionType.SLIDE_LEFT)
        self.panel_revealer.set_reveal_child(True)
        self.panel_revealer.add(self.game_scrolled)

        self.main_box.pack_end(self.panel_revealer, False, False, 0)

        self.view.show()

        self.game_store.load()
        # Contextual menu
        self.view.contextual_menu = ContextualMenu(
            self.game_actions.get_game_actions())

        # Sidebar
        self.sidebar_revealer.set_reveal_child(self.sidebar_visible)
        self.sidebar_revealer.set_transition_duration(300)
        self.update_runtime()

        # Connect account and/or sync
        credentials = api.read_api_key()
        if credentials:
            self.on_connect_success(None, credentials["username"])
        else:
            self.toggle_connection(False)
            self.sync_library()

        self.sync_services()
Esempio n. 5
0
class LutrisWindow(Gtk.ApplicationWindow):
    """Handler class for main window signals."""

    default_view_type = "grid"
    default_width = 800
    default_height = 600

    __gtype_name__ = "LutrisWindow"

    main_box = GtkTemplate.Child()
    games_scrollwindow = GtkTemplate.Child()
    sidebar_revealer = GtkTemplate.Child()
    sidebar_scrolled = GtkTemplate.Child()
    connection_label = GtkTemplate.Child()
    search_revealer = GtkTemplate.Child()
    search_entry = GtkTemplate.Child()
    search_toggle = GtkTemplate.Child()
    zoom_adjustment = GtkTemplate.Child()
    no_results_overlay = GtkTemplate.Child()
    connect_button = GtkTemplate.Child()
    disconnect_button = GtkTemplate.Child()
    register_button = GtkTemplate.Child()
    sync_button = GtkTemplate.Child()
    sync_label = GtkTemplate.Child()
    sync_spinner = GtkTemplate.Child()
    add_popover = GtkTemplate.Child()
    viewtype_icon = GtkTemplate.Child()
    website_search_toggle = GtkTemplate.Child()

    def __init__(self, application, **kwargs):
        width = int(settings.read_setting("width") or self.default_width)
        height = int(settings.read_setting("height") or self.default_height)
        super().__init__(default_width=width,
                         default_height=height,
                         icon_name="lutris",
                         application=application,
                         **kwargs)
        self.application = application
        self.runtime_updater = RuntimeUpdater()
        self.threads_stoppers = []
        self.selected_runner = None
        self.selected_platform = None
        self.icon_type = None

        # Load settings
        self.window_size = (width, height)
        self.maximized = settings.read_setting("maximized") == "True"

        view_type = self.get_view_type()
        self.load_icon_type_from_settings(view_type)

        # Window initialization
        self.game_actions = GameActions(application=application, window=self)

        self.search_terms = None
        self.search_timer_id = None
        self.search_mode = "local"
        self.game_store = self.get_store()
        self.view = self.get_view(view_type)

        GObject.add_emission_hook(Game, "game-updated", self.on_game_updated)
        GObject.add_emission_hook(Game, "game-removed", self.on_game_updated)
        GObject.add_emission_hook(Game, "game-installed",
                                  self.on_game_installed)
        GObject.add_emission_hook(GenericPanel, "running-game-selected",
                                  self.game_selection_changed)
        self.connect("delete-event", self.on_window_delete)
        if self.maximized:
            self.maximize()
        self.init_template()
        self._init_actions()
        self._bind_zoom_adjustment()

        # Load view
        self.games_scrollwindow.add(self.view)
        self._connect_signals()
        # Set theme to dark if set in the settings
        self.set_dark_theme()
        self.set_viewtype_icon(view_type)

        # Add additional widgets
        lutris_icon = Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU)
        lutris_icon.set_margin_right(10)
        lutris_icon.set_margin_left(10)
        self.website_search_toggle.set_image(lutris_icon)
        self.website_search_toggle.set_tooltip_text("Search on Lutris.net")
        self.sidebar_listbox = SidebarListBox()
        self.sidebar_listbox.set_size_request(250, -1)
        self.sidebar_listbox.connect("selected-rows-changed",
                                     self.on_sidebar_changed)
        self.sidebar_scrolled.add(self.sidebar_listbox)

        self.game_panel = GenericPanel(application=self.application)

        self.game_scrolled = Gtk.ScrolledWindow(visible=True)
        self.game_scrolled.set_size_request(320, -1)
        self.game_scrolled.get_style_context().add_class("game-scrolled")
        self.game_scrolled.set_policy(Gtk.PolicyType.EXTERNAL,
                                      Gtk.PolicyType.EXTERNAL)
        self.game_scrolled.add(self.game_panel)

        self.panel_revealer = Gtk.Revealer(visible=True)
        self.panel_revealer.set_transition_duration(300)
        self.panel_revealer.set_transition_type(
            Gtk.RevealerTransitionType.SLIDE_LEFT)
        self.panel_revealer.set_reveal_child(True)
        self.panel_revealer.add(self.game_scrolled)

        self.main_box.pack_end(self.panel_revealer, False, False, 0)

        self.view.show()

        self.game_store.load()
        # Contextual menu
        self.view.contextual_menu = ContextualMenu(
            self.game_actions.get_game_actions())

        # Sidebar
        self.sidebar_revealer.set_reveal_child(self.sidebar_visible)
        self.sidebar_revealer.set_transition_duration(300)
        self.update_runtime()

        # Connect account and/or sync
        credentials = api.read_api_key()
        if credentials:
            self.on_connect_success(None, credentials["username"])
        else:
            self.toggle_connection(False)
            self.sync_library()

        self.sync_services()

        # steamapps_paths = steam.get_steamapps_paths(flat=True)
        # self.steam_watcher = SteamWatcher(steamapps_paths, self.on_steam_game_changed)

    def _init_actions(self):
        Action = namedtuple(
            "Action", ("callback", "type", "enabled", "default", "accel"))
        Action.__new__.__defaults__ = (None, None, True, None, None)

        actions = {
            "browse-games":
            Action(lambda *x: open_uri("https://lutris.net/games/")),
            "register-account":
            Action(lambda *x: open_uri("https://lutris.net/user/register/")),
            "disconnect":
            Action(self.on_disconnect),
            "connect":
            Action(self.on_connect),
            "synchronize":
            Action(lambda *x: self.sync_library()),
            "sync-local":
            Action(lambda *x: self.open_sync_dialog()),
            "add-game":
            Action(self.on_add_game_button_clicked),
            "preferences":
            Action(self.on_preferences_activate),
            "manage-runners":
            Action(self.on_manage_runners),
            "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,
            ),
            "toggle-viewtype":
            Action(self.on_toggle_viewtype),
            "icon-type":
            Action(self.on_icontype_state_change,
                   type="s",
                   default=self.icon_type),
            "view-sorting":
            Action(self.on_view_sorting_state_change,
                   type="s",
                   default=self.view_sorting),
            "view-sorting-ascending":
            Action(
                self.on_view_sorting_direction_change,
                type="b",
                default=self.view_sorting_ascending,
            ),
            "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"

    @property
    def filter_installed(self):
        return settings.read_setting("filter_installed") == "true"

    @property
    def sidebar_visible(self):
        return settings.read_setting("sidebar_visible") in [
            "true",
            None,
        ]

    @property
    def use_dark_theme(self):
        """Return whether to use the dark theme variant (if the theme provides one)"""
        return settings.read_setting("dark_theme",
                                     default="false").lower() == "true"

    @property
    def show_installed_first(self):
        return settings.read_setting("show_installed_first") == "true"

    @property
    def show_tray_icon(self):
        return settings.read_setting("show_tray_icon",
                                     default="false").lower() == "true"

    @property
    def view_sorting(self):
        return settings.read_setting("view_sorting") or "name"

    @property
    def view_sorting_ascending(self):
        return settings.read_setting("view_sorting_ascending") != "false"

    def get_store(self, games=None):
        """Return an instance of GameStore"""
        games = games or pga.get_games(
            show_installed_first=self.show_installed_first)
        game_store = GameStore(
            games,
            self.icon_type,
            self.filter_installed,
            self.view_sorting,
            self.view_sorting_ascending,
            self.show_installed_first,
        )
        game_store.connect("sorting-changed",
                           self.on_game_store_sorting_changed)
        return game_store

    def sync_services(self):
        """Sync local lutris library with current Steam games and desktop games"""
        def full_sync(syncer_cls):
            syncer = syncer_cls()
            games = syncer.load()
            return syncer.sync(games, full=True)

        def on_sync_complete(response, errors):
            """Callback to update the view on sync complete"""
            if errors:
                logger.error("Sync failed: %s", errors)
            added_games, removed_games = response

            for game_id in added_games:
                self.game_store.add_or_update(game_id)

            for game_id in removed_games:
                self.remove_game_from_view(game_id)

        for service in get_services_synced_at_startup():
            AsyncCall(full_sync, on_sync_complete, service.SYNCER)

    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.game_store.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)
                self.game_store.update_game_by_id(game_id)

    def set_dark_theme(self):
        """Enables or disbales dark theme"""
        gtksettings = Gtk.Settings.get_default()
        gtksettings.set_property("gtk-application-prefer-dark-theme",
                                 self.use_dark_theme)

    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.view.connect("game-selected", self.game_selection_changed)
        self.view.connect("game-activated", self.on_game_activated)

    def _bind_zoom_adjustment(self):
        """Bind the zoom slider to the supported banner sizes"""
        image_sizes = list(IMAGE_SIZES.keys())
        self.zoom_adjustment.props.value = image_sizes.index(self.icon_type)
        self.zoom_adjustment.connect(
            "value-changed",
            lambda adj: self._set_icon_type(image_sizes[int(adj.props.value)]),
        )

    @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)

    def get_view_type(self):
        """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 self.default_view_type

    def do_key_press_event(self, event):
        if event.keyval == Gdk.KEY_Escape:
            self.search_toggle.set_active(False)
            return Gdk.EVENT_STOP
        # return Gtk.ApplicationWindow.do_key_press_event(self, event)

        # XXX: This block of code below is to enable searching on type.
        # Enabling this feature steals focus from other entries so it needs
        # some kind of focus detection before enabling library search.

        # Probably not ideal for non-english, but we want to limit
        # which keys actually start searching
        if (not Gdk.KEY_0 <= event.keyval <= Gdk.KEY_z
                or event.state & Gdk.ModifierType.CONTROL_MASK
                or event.state & Gdk.ModifierType.SHIFT_MASK
                or event.state & Gdk.ModifierType.META_MASK
                or event.state & Gdk.ModifierType.MOD1_MASK
                or self.search_entry.has_focus()):
            return Gtk.ApplicationWindow.do_key_press_event(self, event)

        self.search_toggle.set_active(True)
        self.search_entry.grab_focus()
        return self.search_entry.do_key_press_event(self.search_entry, event)

    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 = "icon"
        else:
            self.icon_type = settings.read_setting("icon_type_gridview")
            default = "banner"
        if self.icon_type not in IMAGE_SIZES.keys():
            self.icon_type = default
        return self.icon_type

    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 = ContextualMenu(
            self.game_actions.get_game_actions())
        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()

        self.zoom_adjustment.props.value = list(IMAGE_SIZES.keys()).index(
            self.icon_type)

        self.set_viewtype_icon(view_type)
        settings.write_setting("view_type", view_type)

    def set_viewtype_icon(self, view_type):
        self.viewtype_icon.set_from_icon_name(
            "view-%s-symbolic" % ("list" if view_type == "grid" else "grid"),
            Gtk.IconSize.BUTTON)

    def sync_library(self):
        """Synchronize games with local stuff and server."""
        def update_gui(result, error):
            self.sync_label.set_label("Synchronize library")
            self.sync_spinner.props.active = False
            self.sync_button.set_sensitive(True)
            if error:
                if isinstance(error, http.UnauthorizedAccess):
                    GLib.idle_add(self.show_invalid_credential_warning)
                else:
                    GLib.idle_add(self.show_library_sync_error)
                return
            if result:
                added_ids, updated_ids = result
                self.game_store.add_games_by_ids(added_ids)
                for game_id in updated_ids.difference(added_ids):
                    self.game_store.update_game_by_id(game_id)
            else:
                logger.error("No results returned when syncing the library")

        self.sync_label.set_label("Synchronizing…")
        self.sync_spinner.props.active = True
        self.sync_button.set_sensitive(False)
        AsyncCall(sync_from_remote, update_gui)

    def open_sync_dialog(self):
        """Opens the service sync dialog"""
        self.add_popover.hide()
        SyncServiceWindow(application=self.application)

    def update_runtime(self):
        """Check that the runtime is up to date"""
        runtime_sync = AsyncCall(self.runtime_updater.update, None)
        self.threads_stoppers.append(runtime_sync.stop_request.set)

    def on_dark_theme_state_change(self, action, value):
        """Callback for theme switching action"""
        action.set_state(value)
        settings.write_setting("dark_theme",
                               "true" if value.get_boolean() else "false")
        self.set_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, username):
        """Callback for user connect success"""
        self.toggle_connection(True, username)
        self.sync_library()
        self.actions["synchronize"].props.enabled = True
        self.actions["register-account"].props.enabled = False

    def on_game_activated(self, _widget, game):
        self.game_selection_changed(None, game)
        if game.is_installed:
            self.application.launch(game)
        else:
            InstallerWindow(
                parent=self,
                game_slug=game.slug,
                application=self.application,
            )

    @GtkTemplate.Callback
    def on_disconnect(self, *_args):
        """Callback from user disconnect"""
        dlg = dialogs.QuestionDialog({
            "question": "Do you want to log out from Lutris?",
            "title": "Log out?",
        })
        if dlg.result != Gtk.ResponseType.YES:
            return
        api.disconnect()
        self.toggle_connection(False)
        self.actions["synchronize"].props.enabled = False

    def toggle_connection(self, is_connected, username=None):
        """Sets or unset connected state for the current user"""
        self.connect_button.props.visible = not is_connected
        self.register_button.props.visible = not is_connected
        self.disconnect_button.props.visible = is_connected
        self.sync_button.props.visible = is_connected
        if is_connected:
            self.connection_label.set_text(username)
            logger.info("Connected to lutris.net as %s", username)

    @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()

    def on_window_delete(self, *_args):
        if self.application.running_games.get_n_items():
            dlg = dialogs.QuestionDialog({
                "question": ("Some games are still running, "
                             "are you sure you want to quit Lutris?"),
                "title":
                "Quit Lutris?",
            })
            if dlg.result != Gtk.ResponseType.YES:
                return True

    @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

        # 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)

    @GtkTemplate.Callback
    def on_manage_runners(self, *args):
        return RunnersDialog(transient_for=self)

    def invalidate_game_filter(self):
        """Refilter the game view based on current filters"""
        self.game_store.modelfilter.refilter()
        self.game_store.modelsort.clear_cache()
        self.game_store.sort_view(self.view_sorting,
                                  self.view_sorting_ascending)
        self.no_results_overlay.props.visible = not bool(self.game_store.games)

    def on_show_installed_first_state_change(self, action, value):
        """Callback to handle installed games first toggle"""
        action.set_state(value)
        self.set_show_installed_first_state(value.get_boolean())

    def set_show_installed_first_state(self, show_installed_first):
        """Shows the installed games first in the view"""
        settings.write_setting("show_installed_first",
                               "true" if show_installed_first else "false")
        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)
        self.set_show_installed_state(value.get_boolean())

    def set_show_installed_state(self, filter_installed):
        """Shows or hide uninstalled games"""
        settings.write_setting("filter_installed",
                               "true" if filter_installed else "false")
        self.game_store.filter_installed = filter_installed
        self.invalidate_game_filter()

    @GtkTemplate.Callback
    def on_search_entry_changed(self, entry):
        """Callback for the search input keypresses"""
        if self.search_mode == "local":
            self.game_store.filter_text = entry.get_text()
            self.invalidate_game_filter()
        elif self.search_mode == "website":
            if self.search_timer_id:
                GLib.source_remove(self.search_timer_id)
            self.search_timer_id = GLib.timeout_add(
                750, self.on_search_games_fire,
                entry.get_text().lower().strip())
        else:
            raise ValueError("Unsupported search mode %s" % self.search_mode)

    @GtkTemplate.Callback
    def on_search_toggle(self, button):
        """Called when search bar is shown / hidden"""
        active = button.props.active
        self.search_revealer.set_reveal_child(active)
        if active:
            self.search_entry.grab_focus()
        else:
            self.search_entry.props.text = ""

    @GtkTemplate.Callback
    def on_website_search_toggle_toggled(self, toggle_button):
        self.search_terms = self.search_entry.props.text
        if toggle_button.props.active:
            self.search_mode = "website"
            self.game_store.search_mode = True
            self.search_games(self.search_terms)
        else:
            self.search_mode = "local"
            self.search_games("")

    @GtkTemplate.Callback
    def on_about_clicked(self, *_args):
        """Open the about dialog."""
        dialogs.AboutDialog(parent=self)

    def on_game_error(self, game, error):
        """Called when a game has sent the 'game-error' signal"""
        logger.error("%s crashed", game)
        dialogs.ErrorDialog(error, parent=self)

    def on_game_updated(self, game):
        """Callback to refresh the view when a game is updated"""
        # logger.debug("Updating game %s", game)
        game.load_config()
        try:
            self.game_store.update_game_by_id(game.id)
        except ValueError:
            self.game_store.add_game_by_id(game.id)

        self.view.set_selected_game(game.id)
        self.game_selection_changed(None, game)
        self.sidebar_listbox.update()
        return True

    def on_search_games_fire(self, value):
        GLib.source_remove(self.search_timer_id)
        self.search_timer_id = None
        self.search_games(value)
        return False

    def search_games(self, query):
        """Search for games from the website API"""
        logger.debug("%s search for :%s", self.search_mode, query)
        self.search_terms = query
        self.view.destroy()
        self.game_store = self.get_store(
            api.search_games(query) if query else None)
        self.game_store.set_icon_type(self.icon_type)
        self.game_store.load(from_search=bool(query))
        self.game_store.filter_text = self.search_entry.props.text
        self.switch_view(self.get_view_type())
        self.invalidate_game_filter()

    def game_selection_changed(self, _widget, game):
        """Callback to handle the selection of a game in the view"""
        child = self.game_scrolled.get_child()
        if child:
            self.game_scrolled.remove(child)
            child.destroy()

        if not game:
            self.game_panel = GenericPanel(application=self.application)
        else:
            self.game_actions.set_game(game=game)
            self.game_panel = GamePanel(self.game_actions)
            self.game_panel.connect("panel-closed", self.on_panel_closed)
        self.game_scrolled.add(self.game_panel)
        return True

    def on_panel_closed(self, panel):
        self.game_selection_changed(panel, None)

    def on_game_installed(self, game):
        """Callback to handle newly installed games"""
        self.game_selection_changed(None, game)

    def update_game(self, slug):
        for pga_game in pga.get_games_where(slug=slug):
            self.game_store.update(pga_game)

    @GtkTemplate.Callback
    def on_add_game_button_clicked(self, *_args):
        """Add a new game manually with the AddGameDialog."""
        self.add_popover.hide()
        AddGameDialog(self, runner=self.selected_runner)
        return True

    def remove_game_from_view(self, game_id, from_library=False):
        """Remove a game from the view"""
        self.game_store.update_game_by_id(game_id)
        self.sidebar_listbox.update()

    def on_toggle_viewtype(self, *args):
        self.switch_view("list" if self.current_view_type ==
                         "grid" else "grid")

    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 _set_icon_type(self, icon_type):
        self.icon_type = icon_type
        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 on_icontype_state_change(self, action, value):
        action.set_state(value)
        self._set_icon_type(value.get_string())

    def on_view_sorting_state_change(self, action, value):
        self.game_store.sort_view(value.get_string(),
                                  self.view_sorting_ascending)

    def on_view_sorting_direction_change(self, action, value):
        self.game_store.sort_view(self.view_sorting, value.get_boolean())

    def on_game_store_sorting_changed(self, _game_store, key, ascending):
        self.actions["view-sorting"].set_state(GLib.Variant.new_string(key))
        settings.write_setting("view_sorting", key)

        self.actions["view-sorting-ascending"].set_state(
            GLib.Variant.new_boolean(ascending))
        settings.write_setting("view_sorting_ascending",
                               "true" if ascending else "false")

    def on_sidebar_state_change(self, action, value):
        """Callback to handle siderbar toggle"""
        action.set_state(value)
        sidebar_visible = value.get_boolean()
        settings.write_setting("sidebar_visible",
                               "true" if sidebar_visible else "false")
        self.sidebar_revealer.set_reveal_child(sidebar_visible)
        self.panel_revealer.set_reveal_child(sidebar_visible)
        self.game_scrolled.set_visible(sidebar_visible)

    def on_sidebar_changed(self, widget):
        row = widget.get_selected_row()
        if row is None:
            self.set_selected_filter(None, None)
        elif row.type == "runner":
            self.set_selected_filter(row.id, None)
        else:
            self.set_selected_filter(None, row.id)

    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()

    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.invalidate_game_filter()

    def show_invalid_credential_warning(self):
        dialogs.ErrorDialog(
            "Could not connect to your Lutris account, please sign-in again.")

    def show_library_sync_error(self):
        dialogs.ErrorDialog(
            "Failed to retrieve game library, "
            "there might be some problems contacting lutris.net")
Esempio n. 6
0
    def __init__(self, application, **kwargs):
        width = int(settings.read_setting("width") or self.default_width)
        height = int(settings.read_setting("height") or self.default_height)
        super().__init__(default_width=width,
                         default_height=height,
                         window_position=Gtk.WindowPosition.NONE,
                         name="lutris",
                         icon_name="lutris",
                         application=application,
                         **kwargs)
        update_desktop_icons()
        load_icon_theme()
        self.application = application
        self.window_x = settings.read_setting("window_x")
        self.window_y = settings.read_setting("window_y")
        if self.window_x and self.window_y:
            self.move(int(self.window_x), int(self.window_y))
        self.threads_stoppers = []
        self.window_size = (width, height)
        self.maximized = settings.read_setting("maximized") == "True"
        self.service = None
        self.game_actions = GameActions(application=application, window=self)
        self.search_timer_id = None
        self.selected_category = settings.read_setting("selected_category",
                                                       default="runner:all")
        self.filters = self.load_filters()
        self.set_service(self.filters.get("service"))
        self.icon_type = self.load_icon_type()
        self.game_store = GameStore(self.service, self.service_media)
        self.view = Gtk.Box()

        self.connect("delete-event", self.on_window_delete)
        self.connect("configure-event", self.on_window_configure)
        self.connect("realize", self.on_load)
        if self.maximized:
            self.maximize()

        self.init_template()
        self._init_actions()

        self.set_dark_theme()

        self.set_viewtype_icon(self.view_type)

        lutris_icon = Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU)
        lutris_icon.set_margin_right(3)

        self.sidebar = LutrisSidebar(self.application,
                                     selected=self.selected_category)
        self.sidebar.connect("selected-rows-changed", self.on_sidebar_changed)
        self.sidebar_scrolled.add(self.sidebar)

        self.sidebar_revealer.set_reveal_child(self.side_panel_visible)
        self.sidebar_revealer.set_transition_duration(300)
        self.tabs_box.hide()

        self.game_bar = None
        self.revealer_box = Gtk.HBox(visible=True)
        self.game_revealer.add(self.revealer_box)

        self.connect("view-updated", self.update_store)
        GObject.add_emission_hook(BaseService, "service-login",
                                  self.on_service_login)
        GObject.add_emission_hook(BaseService, "service-logout",
                                  self.on_service_logout)
        GObject.add_emission_hook(BaseService, "service-games-load",
                                  self.on_service_games_updating)
        GObject.add_emission_hook(BaseService, "service-games-loaded",
                                  self.on_service_games_updated)
        GObject.add_emission_hook(Game, "game-updated",
                                  self.on_game_collection_changed)
        GObject.add_emission_hook(Game, "game-removed",
                                  self.on_game_collection_changed)
Esempio n. 7
0
class LutrisWindow(Gtk.ApplicationWindow):  # pylint: disable=too-many-public-methods
    """Handler class for main window signals."""

    default_view_type = "grid"
    default_width = 800
    default_height = 600

    __gtype_name__ = "LutrisWindow"
    __gsignals__ = {
        "view-updated": (GObject.SIGNAL_RUN_FIRST, None, ()),
    }

    tabs_box = GtkTemplate.Child()
    games_scrollwindow = GtkTemplate.Child()
    sidebar_revealer = GtkTemplate.Child()
    sidebar_scrolled = GtkTemplate.Child()
    game_revealer = GtkTemplate.Child()
    search_entry = GtkTemplate.Child()
    zoom_adjustment = GtkTemplate.Child()
    blank_overlay = GtkTemplate.Child()
    viewtype_icon = GtkTemplate.Child()
    library_button = GtkTemplate.Child()
    website_button = GtkTemplate.Child()

    def __init__(self, application, **kwargs):
        width = int(settings.read_setting("width") or self.default_width)
        height = int(settings.read_setting("height") or self.default_height)
        super().__init__(default_width=width,
                         default_height=height,
                         window_position=Gtk.WindowPosition.NONE,
                         name="lutris",
                         icon_name="lutris",
                         application=application,
                         **kwargs)
        update_desktop_icons()
        load_icon_theme()
        self.application = application
        self.window_x = settings.read_setting("window_x")
        self.window_y = settings.read_setting("window_y")
        if self.window_x and self.window_y:
            self.move(int(self.window_x), int(self.window_y))
        self.threads_stoppers = []
        self.window_size = (width, height)
        self.maximized = settings.read_setting("maximized") == "True"
        self.service = None
        self.game_actions = GameActions(application=application, window=self)
        self.search_timer_id = None
        self.selected_category = settings.read_setting("selected_category",
                                                       default="runner:all")
        self.filters = self.load_filters()
        self.set_service(self.filters.get("service"))
        self.icon_type = self.load_icon_type()
        self.game_store = GameStore(self.service, self.service_media)
        self.view = Gtk.Box()

        self.connect("delete-event", self.on_window_delete)
        self.connect("configure-event", self.on_window_configure)
        self.connect("realize", self.on_load)
        if self.maximized:
            self.maximize()

        self.init_template()
        self._init_actions()

        self.set_dark_theme()

        self.set_viewtype_icon(self.view_type)

        lutris_icon = Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU)
        lutris_icon.set_margin_right(3)

        self.sidebar = LutrisSidebar(self.application,
                                     selected=self.selected_category)
        self.sidebar.connect("selected-rows-changed", self.on_sidebar_changed)
        self.sidebar_scrolled.add(self.sidebar)

        self.sidebar_revealer.set_reveal_child(self.side_panel_visible)
        self.sidebar_revealer.set_transition_duration(300)
        self.tabs_box.hide()

        self.game_bar = None
        self.revealer_box = Gtk.HBox(visible=True)
        self.game_revealer.add(self.revealer_box)

        self.connect("view-updated", self.update_store)
        GObject.add_emission_hook(BaseService, "service-login",
                                  self.on_service_login)
        GObject.add_emission_hook(BaseService, "service-logout",
                                  self.on_service_logout)
        GObject.add_emission_hook(BaseService, "service-games-load",
                                  self.on_service_games_updating)
        GObject.add_emission_hook(BaseService, "service-games-loaded",
                                  self.on_service_games_updated)
        GObject.add_emission_hook(Game, "game-updated",
                                  self.on_game_collection_changed)
        GObject.add_emission_hook(Game, "game-removed",
                                  self.on_game_collection_changed)

    def _init_actions(self):
        Action = namedtuple(
            "Action", ("callback", "type", "enabled", "default", "accel"))
        Action.__new__.__defaults__ = (None, None, True, None, None)

        actions = {
            "add-game":
            Action(self.on_add_game_button_clicked),
            "preferences":
            Action(self.on_preferences_activate),
            "manage-runners":
            Action(self.on_manage_runners, ),
            "about":
            Action(self.on_about_clicked),
            "show-installed-only":
            Action(  # delete?
                self.on_show_installed_state_change,
                type="b",
                default=self.filter_installed,
                accel="<Primary>h",
            ),
            "toggle-viewtype":
            Action(self.on_toggle_viewtype),
            "icon-type":
            Action(self.on_icontype_state_change,
                   type="s",
                   default=self.icon_type),
            "view-sorting":
            Action(self.on_view_sorting_state_change,
                   type="s",
                   default=self.view_sorting),
            "view-sorting-ascending":
            Action(
                self.on_view_sorting_direction_change,
                type="b",
                default=self.view_sorting_ascending,
            ),
            "use-dark-theme":
            Action(self.on_dark_theme_state_change,
                   type="b",
                   default=self.use_dark_theme),
            "show-side-panel":
            Action(
                self.on_side_panel_state_change,
                type="b",
                default=self.side_panel_visible,
                accel="F9",
            ),
            "show-hidden-games":
            Action(
                self.hidden_state_change,
                type="b",
                default=self.show_hidden_games,
            ),
            "open-forums":
            Action(lambda *x: open_uri("https://forums.lutris.net/")),
            "open-discord":
            Action(lambda *x: open_uri("https://discord.gg/Pnt5CuY")),
            "donate":
            Action(lambda *x: open_uri("https://lutris.net/donate")),
        }

        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 service_media(self):
        return self.get_service_media(self.load_icon_type())

    def on_load(self, widget, data=None):
        """Finish initializing the view"""
        self.redraw_view()
        self._bind_zoom_adjustment()
        self.view.grab_focus()
        self.view.contextual_menu = ContextualMenu(
            self.game_actions.get_game_actions())

    def load_filters(self):
        """Load the initial filters when creating the view"""
        category, value = self.selected_category.split(":")
        filters = {
            category: value
        }  # Type of filter corresponding to the selected sidebar element
        filters["hidden"] = settings.read_setting(
            "show_hidden_games").lower() == "true"
        filters["installed"] = settings.read_setting(
            "filter_installed").lower() == "true"
        return filters

    def hidden_state_change(self, action, value):
        """Hides or shows the hidden games"""
        action.set_state(value)
        settings.write_setting("show_hidden_games",
                               str(value).lower(),
                               section="lutris")
        self.filters["hidden"] = value
        self.emit("view-updated")

    @property
    def current_view_type(self):
        """Returns which kind of view is currently presented (grid or list)"""
        return settings.read_setting("view_type") or "grid"

    @property
    def filter_installed(self):
        return settings.read_setting("filter_installed").lower() == "true"

    @property
    def side_panel_visible(self):
        return settings.read_setting("side_panel_visible").lower() != "false"

    @property
    def use_dark_theme(self):
        """Return whether to use the dark theme variant (if the theme provides one)"""
        return settings.read_setting("dark_theme",
                                     default="false").lower() == "true"

    @property
    def show_tray_icon(self):
        """Setting to hide or show status icon"""
        return settings.read_setting("show_tray_icon",
                                     default="false").lower() == "true"

    @property
    def view_sorting(self):
        value = settings.read_setting("view_sorting") or "name"
        if value.endswith("_text"):
            value = value[:-5]
        return value

    @property
    def view_sorting_ascending(self):
        return settings.read_setting(
            "view_sorting_ascending").lower() != "false"

    @property
    def show_hidden_games(self):
        return settings.read_setting("show_hidden_games").lower() == "true"

    @property
    def sort_params(self):
        _sort_params = [("installed", "COLLATE NOCASE DESC")]
        _sort_params.append(
            (self.view_sorting, "COLLATE NOCASE ASC"
             if self.view_sorting_ascending else "COLLATE NOCASE DESC"))
        return _sort_params

    def get_running_games(self):
        """Return a list of currently running games"""
        return games_db.get_games_by_ids(
            [game.id for game in self.application.running_games])

    def get_recent_games(self):
        """Return a list of currently running games"""
        searches, _filters, excludes = self.get_sql_filters()
        games = games_db.get_games(searches=searches,
                                   filters={'installed': '1'},
                                   excludes=excludes)
        return sorted(games,
                      key=lambda game: max(game["installed_at"] or 0, game[
                          "lastplayed"] or 0),
                      reverse=True)

    def get_api_games(self):
        """Return games from the lutris API"""
        if not self.filters.get("text"):
            return []
        api_games = api.search_games(self.filters["text"])
        if "icon" in self.icon_type:
            GLib.idle_add(self.load_icons,
                          {g["slug"]: g["icon_url"]
                           for g in api_games}, LutrisIcon)
        else:
            GLib.idle_add(self.load_icons,
                          {g["slug"]: g["banner_url"]
                           for g in api_games}, LutrisBanner)
        return api_games

    def load_icons(self, media_urls, service_media):
        self.game_store.media_loader.download_icons(media_urls,
                                                    service_media())

    def game_matches(self, game):
        if self.filters.get("installed"):
            if game["appid"] not in games_db.get_service_games(
                    self.service.id):
                return False
        if not self.filters.get("text"):
            return True
        return self.filters["text"] in game["name"].lower()

    def set_service(self, service_name):
        if self.service and self.service.id == service_name:
            return self.service
        if not service_name:
            self.service = None
            return
        try:
            self.service = services.get_services()[service_name]()
        except KeyError:
            logger.error("Non existent service '%s'", service_name)
            self.service = None
        return self.service

    @staticmethod
    def combine_games(service_game, lutris_game):
        """Inject lutris game information into a service game"""
        if lutris_game and service_game["appid"] == lutris_game["service_id"]:
            for field in ("platform", "runner", "year", "installed_at",
                          "lastplayed", "playtime", "installed"):
                service_game[field] = lutris_game[field]
        return service_game

    def get_service_games(self, service_name):
        """Switch the current service to service_name and return games if available"""
        service_games = ServiceGameCollection.get_for_service(service_name)
        if service_name == "lutris":
            lutris_games = {g["slug"]: g for g in games_db.get_games()}
        else:
            lutris_games = {
                g["service_id"]: g
                for g in games_db.get_games(
                    filters={"service": self.service.id})
            }

        def get_sort_value(game):
            sort_defaults = {
                "name": "",
                "year": 0,
                "lastplayed": 0.0,
                "installed_at": 0.0,
                "playtime": 0.0,
            }
            lutris_game = lutris_games.get(game["appid"])
            if not lutris_game:
                return sort_defaults[self.view_sorting]
            value = lutris_game[self.view_sorting]
            if value:
                return value
            return sort_defaults[self.view_sorting]

        return [
            self.combine_games(game, lutris_games.get(game["appid"]))
            for game in sorted(service_games,
                               key=get_sort_value,
                               reverse=not self.view_sorting_ascending)
            if self.game_matches(game)
        ]

    def get_games_from_filters(self):
        service_name = self.filters.get("service")
        self.tabs_box.hide()
        if service_name in services.get_services():
            if service_name == "lutris":
                self.tabs_box.show(
                )  # Only the lutris service has the ability to search through all games.
                if self.website_button.props.active:
                    return self.get_api_games()
            if self.service.online and not self.service.is_authenticated():
                self.show_label(
                    _("Connect your %s account to access your games") %
                    self.service.name)
                return []
            return self.get_service_games(service_name)
        dynamic_categories = {
            "recent": self.get_recent_games,
            "running": self.get_running_games,
        }
        if self.filters.get("dynamic_category") in dynamic_categories:
            return dynamic_categories[self.filters["dynamic_category"]]()
        if self.filters.get("category") and self.filters["category"] != "all":
            game_ids = categories_db.get_game_ids_for_category(
                self.filters["category"])
            return games_db.get_games_by_ids(game_ids)
        searches, filters, excludes = self.get_sql_filters()
        return games_db.get_games(searches=searches,
                                  filters=filters,
                                  excludes=excludes,
                                  sorts=self.sort_params)

    def on_service_games_updating(self, service):
        if not self.service or service.id != self.service.id:
            return
        self.show_spinner()
        return True

    def get_sql_filters(self):
        """Return the current filters for the view"""
        sql_filters = {}
        sql_excludes = {}
        if self.filters.get("runner"):
            sql_filters["runner"] = self.filters["runner"]
        if self.filters.get("platform"):
            sql_filters["platform"] = self.filters["platform"]
        if self.filters.get("installed"):
            sql_filters["installed"] = "1"
        if self.filters.get("text"):
            searches = {"name": self.filters["text"]}
        else:
            searches = None
        if not self.filters.get("hidden"):
            sql_excludes["hidden"] = 1
        return searches, sql_filters, sql_excludes

    def get_service_media(self, icon_type):
        """Return the ServiceMedia class used for this view"""
        service = self.service if self.service else LutrisService
        medias = service.medias
        if icon_type in medias:
            return medias[icon_type]()
        return medias[service.default_format]()

    def update_revealer(self, game=None):
        if game:
            if self.game_bar:
                self.game_bar.destroy()
            self.game_bar = GameBar(game, self.game_actions, self.application)
            self.revealer_box.pack_start(self.game_bar, True, True, 0)
        elif self.game_bar:
            # The game bar can't be destroyed here because the game gets unselected on Wayland
            # whenever the game bar is interacted with. Instead, we keep the current game bar open
            # when the game gets unselected, which is somewhat closer to what the intended behavior
            # should be anyway. Might require closing the game bar manually in some cases.
            pass
            # self.game_bar.destroy()
        if self.revealer_box.get_children():
            self.game_revealer.set_reveal_child(True)
        else:
            self.game_revealer.set_reveal_child(False)

    def show_empty_label(self):
        """Display a label when the view is empty"""
        if self.filters.get("text"):
            self.show_label(
                _("No games matching '%s' found ") % self.filters["text"])
        elif self.view.service == "lutris" and self.website_button.props.active:
            self.show_label(_("Use search to find games on lutris.net"))
        else:
            if self.filters.get("category") == "favorite":
                self.show_label(
                    _("Add games to your favorites to see them here."))
            elif self.filters.get("installed"):
                self.show_label(
                    _("No installed games found. Press Ctrl+H so show all games."
                      ))
            else:
                self.show_label(_("No games found"))

    def update_store(self, *_args, **_kwargs):
        self.game_store.store.clear()
        for child in self.blank_overlay.get_children():
            child.destroy()
        games = self.get_games_from_filters()
        self.view.service = self.service.id if self.service else None
        GLib.idle_add(self.update_revealer)
        for game in games:
            self.game_store.add_game(game)
        if not games:
            self.show_empty_label()
        self.search_timer_id = None
        return False

    def set_dark_theme(self):
        """Enables or disables dark theme"""
        gtksettings = Gtk.Settings.get_default()
        gtksettings.set_property("gtk-application-prefer-dark-theme",
                                 self.use_dark_theme)

    def _bind_zoom_adjustment(self):
        """Bind the zoom slider to the supported banner sizes"""
        service = self.service if self.service else LutrisService
        media_services = list(service.medias.keys())
        self.load_icon_type()
        self.zoom_adjustment.set_lower(0)
        self.zoom_adjustment.set_upper(len(media_services) - 1)
        if self.icon_type in media_services:
            value = media_services.index(self.icon_type)
        else:
            value = 0
        self.zoom_adjustment.props.value = value
        self.zoom_adjustment.connect("value-changed", self.on_zoom_changed)

    def on_zoom_changed(self, adjustment):
        """Handler for zoom modification"""
        media_index = round(adjustment.props.value)
        adjustment.props.value = media_index
        service = self.service if self.service else LutrisService
        media_services = list(service.medias.keys())
        if len(media_services) <= media_index:
            media_index = media_services.index(service.default_format)
        icon_type = media_services[media_index]
        if icon_type != self.icon_type:
            self.save_icon_type(icon_type)
            self.show_spinner()
            GLib.timeout_add(100, self._load_icons)

    def _load_icons(self):
        AsyncCall(self.game_store.load_icons, None)
        return False

    def show_label(self, message):
        """Display a label in the middle of the UI"""
        for child in self.blank_overlay.get_children():
            child.destroy()
        label = Gtk.Label(message, visible=True)
        self.blank_overlay.add(label)
        self.blank_overlay.props.visible = True

    def show_spinner(self):
        spinner = Gtk.Spinner(visible=True)
        spinner.start()
        for child in self.blank_overlay.get_children():
            child.destroy()
        self.blank_overlay.add(spinner)
        self.blank_overlay.props.visible = True

    def hide_overlay(self):
        self.blank_overlay.props.visible = False
        for child in self.blank_overlay.get_children():
            child.destroy()

    @property
    def view_type(self):
        """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 self.default_view_type

    def do_key_press_event(self, event):  # pylint: disable=arguments-differ
        # XXX: This block of code below is to enable searching on type.
        # Enabling this feature steals focus from other entries so it needs
        # some kind of focus detection before enabling library search.

        # Probably not ideal for non-english, but we want to limit
        # which keys actually start searching
        if event.keyval == Gdk.KEY_Escape:
            self.search_entry.set_text("")
            self.view.grab_focus()
            return Gtk.ApplicationWindow.do_key_press_event(self, event)

        if (  # pylint: disable=too-many-boolean-expressions
                not Gdk.KEY_0 <= event.keyval <= Gdk.KEY_z
                or event.state & Gdk.ModifierType.CONTROL_MASK
                or event.state & Gdk.ModifierType.SHIFT_MASK
                or event.state & Gdk.ModifierType.META_MASK
                or event.state & Gdk.ModifierType.MOD1_MASK
                or self.search_entry.has_focus()):
            return Gtk.ApplicationWindow.do_key_press_event(self, event)
        self.search_entry.grab_focus()
        return self.search_entry.do_key_press_event(self.search_entry, event)

    def load_icon_type(self):
        """Return the icon style depending on the type of view."""
        setting_key = "icon_type_%sview" % self.current_view_type
        if self.service and self.service.id != "lutris":
            setting_key += "_%s" % self.service.id
        self.icon_type = settings.read_setting(setting_key)
        return self.icon_type

    def save_icon_type(self, icon_type):
        self.icon_type = icon_type
        setting_key = "icon_type_%sview" % self.current_view_type
        if self.service and self.service.id != "lutris":
            setting_key += "_%s" % self.service.id
        settings.write_setting(setting_key, self.icon_type)
        self.redraw_view()

    def redraw_view(self):
        """Completely reconstruct the main view"""
        if not self.game_store:
            logger.error("No game store yet")
            return
        if self.view:
            self.view.destroy()
        self.game_store = GameStore(self.service, self.service_media)
        if self.view_type == "grid":
            self.view = GameGridView(self.game_store,
                                     self.game_store.service_media,
                                     hide_text=settings.read_setting(
                                         "hide_text_under_icons") == "True")
        else:
            self.view = GameListView(self.game_store,
                                     self.game_store.service_media)

        self.view.connect("game-selected", self.on_game_selection_changed)
        self.view.connect("game-activated", self.on_game_activated)
        self.view.contextual_menu = ContextualMenu(
            self.game_actions.get_game_actions())
        for child in self.games_scrollwindow.get_children():
            child.destroy()
        self.games_scrollwindow.add(self.view)

        self.view.show_all()
        self.view.grab_focus()
        GLib.idle_add(self.update_store)

    def set_viewtype_icon(self, view_type):
        self.viewtype_icon.set_from_icon_name("view-%s-symbolic" % view_type,
                                              Gtk.IconSize.BUTTON)

    def set_show_installed_state(self, filter_installed):
        """Shows or hide uninstalled games"""
        settings.write_setting("filter_installed", bool(filter_installed))
        self.filters["installed"] = filter_installed

    def on_service_games_updated(self, service):
        """Request a view update when service games are loaded"""
        if self.service and service.id == self.service.id:
            self.emit("view-updated")
        return True

    def on_service_login(self, service):
        AsyncCall(service.load, None)
        return True

    def on_service_logout(self, service):
        if self.service and service.id == self.service.id:
            self.emit("view-updated")
        return True

    def on_dark_theme_state_change(self, action, value):
        """Callback for theme switching action"""
        action.set_state(value)
        settings.write_setting("dark_theme", value.get_boolean())
        self.set_dark_theme()

    @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()
        size = widget.get_size()
        if not self.maximized:
            self.window_size = size
        self.search_entry.set_size_request(min(max(50, size[0] - 470), 800),
                                           -1)

    def on_window_delete(self, *_args):
        if self.application.running_games.get_n_items():
            self.hide()
            return True

    def on_window_configure(self, *_args):
        """Callback triggered when the window is moved, resized..."""
        self.window_x, self.window_y = self.get_position()

    @GtkTemplate.Callback
    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)
        if self.window_x and self.window_y:
            settings.write_setting("window_x", self.window_x)
            settings.write_setting("window_y", self.window_y)
        settings.write_setting("maximized", self.maximized)

    @GtkTemplate.Callback
    def on_preferences_activate(self, *_args):
        """Callback when preferences is activated."""
        self.application.show_window(SystemConfigDialog)

    @GtkTemplate.Callback
    def on_manage_runners(self, *args):
        self.application.show_window(RunnersDialog, transient_for=self)

    def on_show_installed_state_change(self, action, value):
        """Callback to handle uninstalled game filter switch"""
        action.set_state(value)
        self.set_show_installed_state(value.get_boolean())
        self.emit("view-updated")

    @GtkTemplate.Callback
    def on_search_entry_changed(self, entry):
        """Callback for the search input keypresses"""
        if self.search_timer_id:
            GLib.source_remove(self.search_timer_id)
        self.filters["text"] = entry.get_text().lower().strip()
        if self.service and self.service.id == "lutris" and self.website_button.props.active:
            delay = 1250  # Big delay to make sure user has stopped typing before sending a search
        else:
            delay = 150
        self.search_timer_id = GLib.timeout_add(delay, self.update_store)

    @GtkTemplate.Callback
    def on_search_entry_key_press(self, widget, event):
        if event.keyval == Gdk.KEY_Down:
            if self.current_view_type == 'grid':
                self.view.select_path(
                    Gtk.TreePath('0'))  # needed for gridview only
                # if game_bar is alive at this point it can mess grid item selection up
                # for some unknown reason,
                # it is safe to close it here, it will be reopened automatically.
                if self.game_bar:
                    self.game_bar.destroy()  # for gridview only
            self.view.set_cursor(Gtk.TreePath('0'), None,
                                 False)  # needed for both view types
            self.view.grab_focus()

    @GtkTemplate.Callback
    def on_about_clicked(self, *_args):
        """Open the about dialog."""
        dialogs.AboutDialog(parent=self)

    def on_game_error(self, game, error):
        """Called when a game has sent the 'game-error' signal"""
        logger.error("%s crashed", game)
        dialogs.ErrorDialog(error, parent=self)

    @GtkTemplate.Callback
    def on_add_game_button_clicked(self, *_args):
        """Add a new game manually with the AddGameDialog."""
        if "runner" in self.filters:
            runner = self.filters["runner"]
        else:
            runner = None
        AddGameDialog(self, runner=runner)
        return True

    def on_toggle_viewtype(self, *args):
        view_type = "list" if self.current_view_type == "grid" else "grid"
        self.set_viewtype_icon(view_type)
        settings.write_setting("view_type", view_type)
        self.redraw_view()

    def on_icontype_state_change(self, action, value):
        action.set_state(value)
        self._set_icon_type(value.get_string())

    def on_view_sorting_state_change(self, action, value):
        self.actions["view-sorting"].set_state(value)
        value = str(value).strip("'")
        settings.write_setting("view_sorting", value)
        self.emit("view-updated")

    def on_view_sorting_direction_change(self, action, value):
        self.actions["view-sorting-ascending"].set_state(value)
        settings.write_setting("view_sorting_ascending", bool(value))
        self.emit("view-updated")

    def on_side_panel_state_change(self, action, value):
        """Callback to handle side panel toggle"""
        action.set_state(value)
        side_panel_visible = value.get_boolean()
        settings.write_setting("side_panel_visible", bool(side_panel_visible))
        self.sidebar_revealer.set_reveal_child(side_panel_visible)

    def on_sidebar_changed(self, widget):
        row = widget.get_selected_row()
        self.selected_category = "%s:%s" % (row.type, row.id)
        for filter_type in ("category", "dynamic_category", "service",
                            "runner", "platform"):
            if filter_type in self.filters:
                self.filters.pop(filter_type)
        if row:
            self.filters[row.type] = row.id
        service_name = self.filters.get("service")
        self.set_service(service_name)
        self._bind_zoom_adjustment()
        self.redraw_view()

    def on_game_selection_changed(self, view, selection):
        if not selection:
            GLib.idle_add(self.update_revealer)
            return False
        game_id = view.get_model().get_value(selection, COL_ID)
        if not game_id:
            GLib.idle_add(self.update_revealer)
            return False
        if self.service:
            game = ServiceGameCollection.get_game(self.service.id, game_id)
        else:
            game = games_db.get_game_by_field(int(game_id), "id")
        if not game:
            game = {
                "id": game_id,
                "appid": game_id,
                "name": view.get_model().get_value(selection, COL_NAME),
                "slug": game_id,
                "service": self.service.id if self.service else None,
            }

        GLib.idle_add(self.update_revealer, game)
        return False

    def on_game_collection_changed(self, _sender):
        """Simple method used to refresh the view"""
        logger.debug("Game collection changed")
        self.emit("view-updated")
        return True

    def on_game_activated(self, view, game_id):
        """Handles view activations (double click, enter press)"""
        if self.service:
            if self.service.id != "lutris":
                db_game = games_db.get_game_for_service(
                    self.service.id, game_id)
                if db_game:
                    game_id = db_game["id"]
                else:
                    db_game = ServiceGameCollection.get_game(
                        self.service.id, game_id)
                    if db_game:
                        game_id = self.service.install(db_game)
                    else:
                        game_id = self.service.install(game_id)
            else:
                db_game = games_db.get_game_by_field(game_id)
                if not db_game:
                    self.service.install(game_id)
                    return
                if db_game["installed"] != 1:
                    self.service.install(game_id)
                    return
                game_id = db_game["id"]
        if game_id:
            game = Game(game_id)
            game.emit("game-launch")
Esempio n. 8
0
class LutrisWindow(Gtk.ApplicationWindow):  # pylint: disable=too-many-public-methods
    """Handler class for main window signals."""

    default_view_type = "grid"
    default_width = 800
    default_height = 600

    __gtype_name__ = "LutrisWindow"
    __gsignals__ = {
        "view-updated": (GObject.SIGNAL_RUN_FIRST, None, ()),
    }

    main_box = GtkTemplate.Child()
    games_scrollwindow = GtkTemplate.Child()
    sidebar_revealer = GtkTemplate.Child()
    sidebar_scrolled = GtkTemplate.Child()
    game_revealer = GtkTemplate.Child()
    search_entry = GtkTemplate.Child()
    search_toggle = GtkTemplate.Child()
    zoom_adjustment = GtkTemplate.Child()
    blank_overlay = GtkTemplate.Child()
    viewtype_icon = GtkTemplate.Child()

    def __init__(self, application, **kwargs):
        width = int(settings.read_setting("width") or self.default_width)
        height = int(settings.read_setting("height") or self.default_height)
        super().__init__(default_width=width,
                         default_height=height,
                         window_position=Gtk.WindowPosition.NONE,
                         icon_name="lutris",
                         application=application,
                         **kwargs)
        self.application = application
        self.runtime_updater = RuntimeUpdater()
        self.threads_stoppers = []
        self.icon_type = None
        self.service = None

        self.window_size = (width, height)
        self.maximized = settings.read_setting("maximized") == "True"

        icon_type = self.load_icon_type()
        self.service_media = self.get_service_media(icon_type)

        self.game_actions = GameActions(application=application, window=self)
        self.search_timer_id = None
        self.game_store = None
        self.view = Gtk.Box()

        self.connect("delete-event", self.on_window_delete)
        self.connect("map-event", self.on_load)
        if self.maximized:
            self.maximize()
        self.init_template()
        self._init_actions()

        self.set_dark_theme()
        self.set_viewtype_icon(self.view_type)

        lutris_icon = Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU)
        lutris_icon.set_margin_right(3)
        self.selected_category = settings.read_setting("selected_category",
                                                       default="runner:all")

        self.filters = self.load_filters()

        self.sidebar = LutrisSidebar(self.application,
                                     selected=self.selected_category)
        self.sidebar.set_size_request(250, -1)
        self.sidebar.connect("selected-rows-changed", self.on_sidebar_changed)
        self.sidebar_scrolled.add(self.sidebar)

        self.sidebar_revealer.set_reveal_child(self.left_side_panel_visible)
        self.sidebar_revealer.set_transition_duration(300)

        self.game_bar = None
        self.revealer_box = Gtk.HBox(visible=True)
        self.game_revealer.add(self.revealer_box)

        GObject.add_emission_hook(BaseService, "service-login",
                                  self.on_service_login)
        GObject.add_emission_hook(BaseService, "service-logout",
                                  self.on_service_logout)
        GObject.add_emission_hook(BaseService, "service-games-load",
                                  self.on_service_games_updating)
        GObject.add_emission_hook(BaseService, "service-games-loaded",
                                  self.on_service_games_updated)

    def _init_actions(self):
        Action = namedtuple(
            "Action", ("callback", "type", "enabled", "default", "accel"))
        Action.__new__.__defaults__ = (None, None, True, None, None)

        actions = {
            "add-game":
            Action(self.on_add_game_button_clicked),
            "preferences":
            Action(self.on_preferences_activate),
            "manage-runners":
            Action(self.on_manage_runners, ),
            "about":
            Action(self.on_about_clicked),
            "show-installed-only":
            Action(  # delete?
                self.on_show_installed_state_change,
                type="b",
                default=self.filter_installed,
                accel="<Primary>h",
            ),
            "toggle-viewtype":
            Action(self.on_toggle_viewtype),
            "icon-type":
            Action(self.on_icontype_state_change,
                   type="s",
                   default=self.icon_type),
            "view-sorting":
            Action(self.on_view_sorting_state_change,
                   type="s",
                   default=self.view_sorting),
            "view-sorting-ascending":
            Action(
                self.on_view_sorting_direction_change,
                type="b",
                default=self.view_sorting_ascending,
            ),
            "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-left-side-panel":
            Action(
                self.on_left_side_panel_state_change,
                type="b",
                default=self.left_side_panel_visible,
                accel="F9",
            ),
            "show-hidden-games":
            Action(
                self.hidden_state_change,
                type="b",
                default=self.show_hidden_games,
            ),
            "open-forums":
            Action(lambda *x: open_uri("https://forums.lutris.net/")),
            "open-discord":
            Action(lambda *x: open_uri("https://discord.gg/Pnt5CuY")),
            "donate":
            Action(lambda *x: open_uri("https://lutris.net/donate")),
        }

        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)

    def on_load(self, widget, data):
        self.game_store = GameStore(self.service_media)
        self.redraw_view()
        self._bind_zoom_adjustment()
        self.view.grab_focus()
        self.view.contextual_menu = ContextualMenu(
            self.game_actions.get_game_actions())
        self.update_runtime()

    def load_filters(self):
        """Load the initial filters when creating the view"""
        category, value = self.selected_category.split(":")
        filters = {
            category: value
        }  # Type of filter corresponding to the selected sidebar element
        filters["hidden"] = settings.read_setting(
            "show_hidden_games").lower() == "true"
        filters["installed"] = settings.read_setting(
            "filter_installed").lower() == "true"
        return filters

    def hidden_state_change(self, action, value):
        """Hides or shows the hidden games"""
        action.set_state(value)
        settings.write_setting("show_hidden_games",
                               str(value).lower(),
                               section="lutris")
        self.filters["hidden"] = value
        self.emit("view-updated")

    @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"

    @property
    def filter_installed(self):
        return settings.read_setting("filter_installed").lower() == "true"

    @property
    def left_side_panel_visible(self):
        show_left_panel = (
            settings.read_setting("left_side_panel_visible").lower() !=
            "false")
        return show_left_panel or self.sidebar_visible

    @property
    def sidebar_visible(self):
        """Deprecated: For compability only"""
        return settings.read_setting("sidebar_visible") in [
            "true",
            None,
        ]

    @property
    def use_dark_theme(self):
        """Return whether to use the dark theme variant (if the theme provides one)"""
        return settings.read_setting("dark_theme",
                                     default="false").lower() == "true"

    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()

    @property
    def show_tray_icon(self):
        """Setting to hide or show status icon"""
        return settings.read_setting("show_tray_icon",
                                     default="false").lower() == "true"

    @property
    def view_sorting(self):
        value = settings.read_setting("view_sorting") or "name"
        if value.endswith("_text"):
            value = value[:-5]
        return value

    @property
    def view_sorting_ascending(self):
        return settings.read_setting(
            "view_sorting_ascending").lower() != "false"

    @property
    def show_hidden_games(self):
        return settings.read_setting("show_hidden_games").lower() == "true"

    @property
    def sort_params(self):
        _sort_params = [("installed", "DESC")]
        _sort_params.append((self.view_sorting,
                             "ASC" if self.view_sorting_ascending else "DESC"))
        return _sort_params

    def get_running_games(self):
        """Return a list of currently running games"""
        return games_db.get_games_by_ids(
            [game.id for game in self.application.running_games])

    def get_installed_games(self):
        """Return a list of currently running games"""
        searches, _filters, excludes = self.get_sql_filters()
        return games_db.get_games(searches=searches,
                                  filters={'installed': '1'},
                                  excludes=excludes)

    def get_api_games(self):
        """Return games from the lutris API"""
        if not self.filters.get("text"):
            api_games = api.get_bundle("featured")
        else:
            api_games = api.search_games(self.filters["text"])
        return api_games

    def game_matches(self, game):
        if self.filters.get("installed"):
            if game["appid"] not in games_db.get_service_games(
                    self.service.id):
                return False
        if not self.filters.get("text"):
            return True
        return self.filters["text"] in game["name"].lower()

    def set_service(self, service_name):
        self.service = services.get_services()[service_name]()

    def unset_service(self):
        self.service = None

    def get_games_from_filters(self):
        service_name = self.filters.get("service")
        if service_name in services.get_services():
            self.set_service(service_name)
            service_games = ServiceGameCollection.get_for_service(service_name)
            if service_games:
                return [
                    game
                    for game in sorted(service_games,
                                       key=lambda game: game.get(
                                           self.view_sorting) or game["name"],
                                       reverse=not self.view_sorting_ascending)
                    if self.game_matches(game)
                ]
            if self.service.online and not self.service.is_connected():
                self.show_label(
                    _("Connect your %s account to access your games") %
                    self.service.name)
            return
        self.unset_service()
        dynamic_categories = {
            "running": self.get_running_games,
            "installed": self.get_installed_games
        }
        if self.filters.get("dynamic_category") in dynamic_categories:
            return dynamic_categories[self.filters["dynamic_category"]]()
        if self.filters.get("category"):
            game_ids = categories_db.get_game_ids_for_category(
                self.filters["category"])
            return games_db.get_games_by_ids(game_ids)
        searches, filters, excludes = self.get_sql_filters()
        return games_db.get_games(searches=searches,
                                  filters=filters,
                                  excludes=excludes,
                                  sorts=self.sort_params)

    def on_service_games_updating(self, service):
        if not self.service or service.id != self.service.id:
            logger.warning("Wrong service %s", self.service)
            return
        self.show_spinner()

    def get_sql_filters(self):
        """Return the current filters for the view"""
        sql_filters = {}
        sql_excludes = {}
        if self.filters.get("runner"):
            sql_filters["runner"] = self.filters["runner"]
        if self.filters.get("platform"):
            sql_filters["platform"] = self.filters["platform"]
        if self.filters.get("installed"):
            sql_filters["installed"] = "1"
        if self.filters.get("text"):
            searches = {"name": self.filters["text"]}
        else:
            searches = None
        if not self.filters.get("hidden"):
            sql_excludes["hidden"] = 1
        return searches, sql_filters, sql_excludes

    def get_service_media(self, icon_type):
        """Return the ServiceMedia class used for this view"""
        service = self.service if self.service else LutrisService
        medias = service.medias
        if icon_type in medias:
            return medias[icon_type]()
        return medias[service.default_format]()

    def update_revealer(self, game=None):
        if game:
            if self.game_bar:
                self.game_bar.destroy()
            self.game_bar = GameBar(game, self.game_actions)
            self.revealer_box.pack_start(self.game_bar, True, True, 0)
        elif self.game_bar:
            self.game_bar.destroy()
        if self.revealer_box.get_children():
            self.game_revealer.set_reveal_child(True)
        else:
            self.game_revealer.set_reveal_child(False)

    def update_store(self, *_args, **_kwargs):
        self.game_store.store.clear()
        for child in self.blank_overlay.get_children():
            child.destroy()
        games = self.get_games_from_filters()
        self.view.service = self.service.id if self.service else None
        self.reload_service_media()
        self.update_revealer()
        if games is None:
            return False
        for game in games:
            game["image_size"] = self.service_media.size
            self.game_store.add_game(game)
        if not games:
            if self.filters.get("text"):
                self.show_label(
                    _("No games matching '%s' found ") % self.filters["text"])
            else:
                self.show_label(_("No games found"))
        self.search_timer_id = None
        return False

    def set_dark_theme(self):
        """Enables or disables dark theme"""
        gtksettings = Gtk.Settings.get_default()
        gtksettings.set_property("gtk-application-prefer-dark-theme",
                                 self.use_dark_theme)

    def _bind_zoom_adjustment(self):
        """Bind the zoom slider to the supported banner sizes"""
        service = self.service if self.service else LutrisService
        media_services = list(service.medias.keys())
        self.zoom_adjustment.set_lower(0)
        self.zoom_adjustment.set_upper(len(media_services) - 1)
        if self.icon_type in media_services:
            value = media_services.index(self.icon_type)
        else:
            value = 0
        self.zoom_adjustment.props.value = value
        self.zoom_adjustment.connect("value-changed", self.on_zoom_changed)

    def on_zoom_changed(self, adjustment):
        """Handler for zoom modification"""
        media_index = round(adjustment.props.value)
        adjustment.props.value = media_index
        service = self.service if self.service else LutrisService
        media_services = list(service.medias.keys())
        if len(media_services) <= media_index:
            media_index = media_services.index(service.default_format)
        icon_type = media_services[media_index]
        if icon_type != self.icon_type:
            self.save_icon_type(icon_type)
            self.reload_service_media()
            self.show_spinner()
            AsyncCall(self.game_store.load_icons, self.icons_loaded_cb)

    def show_label(self, message):
        """Display a label in the middle of the UI"""
        for child in self.blank_overlay.get_children():
            child.destroy()
        label = Gtk.Label(message)
        self.blank_overlay.add(label)
        self.blank_overlay.props.visible = True

    def show_spinner(self):
        spinner = Gtk.Spinner(visible=True)
        spinner.start()
        for child in self.blank_overlay.get_children():
            child.destroy()
        self.blank_overlay.add(spinner)
        self.blank_overlay.props.visible = True

    def hide_overlay(self):
        self.blank_overlay.props.visible = False
        for child in self.blank_overlay.get_children():
            child.destroy()

    def icons_loaded_cb(self, result, error):
        if error:
            logger.debug("Failed to reload icons")
        self.hide_overlay()
        self.emit("view-updated")

    @property
    def view_type(self):
        """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 self.default_view_type

    def do_key_press_event(self, event):  # pylint: disable=arguments-differ
        if event.keyval == Gdk.KEY_Escape:
            self.search_toggle.set_active(False)
            return Gdk.EVENT_STOP
        # XXX: This block of code below is to enable searching on type.
        # Enabling this feature steals focus from other entries so it needs
        # some kind of focus detection before enabling library search.

        # Probably not ideal for non-english, but we want to limit
        # which keys actually start searching
        if (  # pylint: disable=too-many-boolean-expressions
                not Gdk.KEY_0 <= event.keyval <= Gdk.KEY_z
                or event.state & Gdk.ModifierType.CONTROL_MASK
                or event.state & Gdk.ModifierType.SHIFT_MASK
                or event.state & Gdk.ModifierType.META_MASK
                or event.state & Gdk.ModifierType.MOD1_MASK
                or self.search_entry.has_focus()):
            return Gtk.ApplicationWindow.do_key_press_event(self, event)

        self.search_toggle.set_active(True)
        self.search_entry.grab_focus()
        return self.search_entry.do_key_press_event(self.search_entry, event)

    def load_icon_type(self):
        """Return the icon style depending on the type of view."""
        if self.view_type == "list":
            self.icon_type = settings.read_setting("icon_type_listview")
        else:
            self.icon_type = settings.read_setting("icon_type_gridview")
        return self.icon_type

    def save_icon_type(self, icon_type):
        self.icon_type = icon_type
        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.redraw_view()

    def reload_service_media(self):
        self.game_store.set_service_media(
            self.get_service_media(self.load_icon_type()))

    def redraw_view(self):
        """Completely reconstruct the main view"""
        if self.view:
            self.view.destroy()
        self.reload_service_media()

        view_class = GameGridView if self.view_type == "grid" else GameListView
        self.view = view_class(self.game_store, self.game_store.service_media)

        self.view.connect("game-selected", self.on_game_selection_changed)
        self.view.connect("game-activated", self.on_game_activated)
        self.view.contextual_menu = ContextualMenu(
            self.game_actions.get_game_actions())
        for child in self.games_scrollwindow.get_children():
            child.destroy()
        self.games_scrollwindow.add(self.view)
        self.connect("view-updated", self.update_store)

        self.view.show_all()
        self.update_store()

    def set_viewtype_icon(self, view_type):
        self.viewtype_icon.set_from_icon_name("view-%s-symbolic" % view_type,
                                              Gtk.IconSize.BUTTON)

    def update_runtime(self):
        """Check that the runtime is up to date"""
        runtime_sync = AsyncCall(self.runtime_updater.update, None)
        self.threads_stoppers.append(runtime_sync.stop_request.set)

    def set_show_installed_state(self, filter_installed):
        """Shows or hide uninstalled games"""
        settings.write_setting("filter_installed", bool(filter_installed))
        self.filters["installed"] = filter_installed
        self.emit("view-updated")

    def on_service_games_updated(self, service):
        AsyncCall(self.game_store.load_icons, None)
        self.emit("view-updated")

    def on_service_login(self, service):
        AsyncCall(service.load, None)

    def on_service_logout(self, service):
        self.emit("view-updated")

    def on_dark_theme_state_change(self, action, value):
        """Callback for theme switching action"""
        action.set_state(value)
        settings.write_setting("dark_theme", value.get_boolean())
        self.set_dark_theme()

    @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()

    def on_window_delete(self, *_args):
        if self.application.running_games.get_n_items():
            self.hide()
            return True

    @GtkTemplate.Callback
    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)
        settings.write_setting("maximized", self.maximized)

    @GtkTemplate.Callback
    def on_preferences_activate(self, *_args):
        """Callback when preferences is activated."""
        self.application.show_window(SystemConfigDialog)

    @GtkTemplate.Callback
    def on_manage_runners(self, *args):
        self.application.show_window(RunnersDialog, transient_for=self)

    def on_show_installed_state_change(self, action, value):
        """Callback to handle uninstalled game filter switch"""
        action.set_state(value)
        self.set_show_installed_state(value.get_boolean())
        self.emit("view-updated")

    @GtkTemplate.Callback
    def on_search_entry_changed(self, entry):
        """Callback for the search input keypresses"""
        if self.search_timer_id:
            GLib.source_remove(self.search_timer_id)
        self.filters["text"] = entry.get_text().lower().strip()
        self.search_timer_id = GLib.timeout_add(350, self.update_store)

    @GtkTemplate.Callback
    def on_search_toggle(self, button):
        """Called when search bar is shown / hidden"""
        active = button.props.active
        if active:
            self.search_entry.show()
            self.search_entry.grab_focus()
        else:
            self.search_entry.props.text = ""
            self.search_entry.hide()

    @GtkTemplate.Callback
    def on_about_clicked(self, *_args):
        """Open the about dialog."""
        dialogs.AboutDialog(parent=self)

    def on_game_error(self, game, error):
        """Called when a game has sent the 'game-error' signal"""
        logger.error("%s crashed", game)
        dialogs.ErrorDialog(error, parent=self)

    @GtkTemplate.Callback
    def on_add_game_button_clicked(self, *_args):
        """Add a new game manually with the AddGameDialog."""
        if "runner" in self.filters:
            runner = self.filters["runner"]
        else:
            runner = None
        AddGameDialog(self, runner=runner)
        return True

    def on_toggle_viewtype(self, *args):
        view_type = "list" if self.current_view_type == "grid" else "grid"
        self.set_viewtype_icon(view_type)
        settings.write_setting("view_type", view_type)
        self.redraw_view()

    def on_icontype_state_change(self, action, value):
        action.set_state(value)
        self._set_icon_type(value.get_string())

    def on_view_sorting_state_change(self, action, value):
        self.actions["view-sorting"].set_state(value)
        value = str(value).strip("'")
        settings.write_setting("view_sorting", value)
        self.emit("view-updated")

    def on_view_sorting_direction_change(self, action, value):
        self.actions["view-sorting-ascending"].set_state(value)
        settings.write_setting("view_sorting_ascending", bool(value))
        self.emit("view-updated")

    def on_left_side_panel_state_change(self, action, value):
        """Callback to handle left side panel toggle"""
        action.set_state(value)
        left_side_panel_visible = value.get_boolean()
        settings.write_setting("left_side_panel_visible",
                               bool(left_side_panel_visible))
        self.sidebar_revealer.set_reveal_child(left_side_panel_visible)
        # Retrocompatibility with sidebar_visible :
        # if we change the new attribute, we must set the old one to false
        if self.sidebar_visible:
            settings.write_setting("sidebar_visible", "false")

    def on_sidebar_changed(self, widget):
        row = widget.get_selected_row()
        self.set_title("Lutris - %s" % row.name)
        self.selected_category = "%s:%s" % (row.type, row.id)
        for filter_type in ("category", "dynamic_category", "service",
                            "runner", "platform"):
            if filter_type in self.filters:
                self.filters.pop(filter_type)
        if row:
            self.filters[row.type] = row.id
        self.emit("view-updated")

    def on_game_selection_changed(self, view, game_id):
        if not game_id:
            GLib.idle_add(self.update_revealer)
            return False
        if self.service:
            game = ServiceGameCollection.get_game(self.service.id, game_id)
        else:
            game = games_db.get_game_by_field(int(game_id), "id")
        GLib.idle_add(self.update_revealer, game)
        return False

    def on_game_activated(self, view, game_id):
        """Handles view activations (double click, enter press)"""
        if self.service:
            db_game = games_db.get_game_for_service(self.service.id, game_id)
            if db_game:
                game_id = db_game["id"]
            else:
                db_game = ServiceGameCollection.get_game(
                    self.service.id, game_id)
                self.service.install(db_game)
                return
        game = Game(game_id)
        if game.is_installed:
            logger.info("Game is installed")
        game.emit("game-launch")
Esempio n. 9
0
    def __init__(self, application, **kwargs):
        self.application = application
        self.runtime_updater = RuntimeUpdater()
        self.threads_stoppers = []
        self.selected_runner = None
        self.selected_platform = None
        self.icon_type = None

        # Load settings
        width = int(settings.read_setting("width") or self.default_width)
        height = int(settings.read_setting("height") or self.default_height)
        self.window_size = (width, height)
        self.maximized = settings.read_setting("maximized") == "True"

        view_type = self.get_view_type()
        self.load_icon_type_from_settings(view_type)

        # Window initialization
        self.game_actions = GameActions(application=application, window=self)
        self.game_store = GameStore(
            self.icon_type,
            self.filter_installed,
            self.view_sorting,
            self.view_sorting_ascending,
            self.show_installed_first,
        )
        self.view = self.get_view(view_type)
        self.game_store.connect("sorting-changed",
                                self.on_game_store_sorting_changed)
        super().__init__(default_width=width,
                         default_height=height,
                         icon_name="lutris",
                         application=application,
                         **kwargs)
        if self.maximized:
            self.maximize()
        self.init_template()
        self._init_actions()
        self._bind_zoom_adjustment()

        # Load view
        self.games_scrollwindow.add(self.view)
        self._connect_signals()
        # Set theme to dark if set in the settings
        self.set_dark_theme()
        self.set_viewtype_icon(view_type)

        # Add additional widgets
        self.sidebar_listbox = SidebarListBox()
        self.sidebar_listbox.set_size_request(250, -1)
        self.sidebar_listbox.connect("selected-rows-changed",
                                     self.on_sidebar_changed)
        self.sidebar_scrolled.add(self.sidebar_listbox)

        self.game_scrolled = Gtk.ScrolledWindow(visible=True)
        self.game_scrolled.set_size_request(320, -1)
        self.game_scrolled.get_style_context().add_class("game-scrolled")
        self.game_scrolled.set_policy(Gtk.PolicyType.EXTERNAL,
                                      Gtk.PolicyType.EXTERNAL)

        self.game_panel = Gtk.Box()
        self.main_box.pack_end(self.game_scrolled, False, False, 0)

        self.view.show()

        # Contextual menu
        self.view.contextual_menu = ContextualMenu(
            self.game_actions.get_game_actions())

        self.game_store.load()

        # Sidebar
        self.sidebar_revealer.set_reveal_child(self.sidebar_visible)
        self.update_runtime()

        # Connect account and/or sync
        credentials = api.read_api_key()
        if credentials:
            self.on_connect_success(None, credentials["username"])
        else:
            self.toggle_connection(False)
            self.sync_library()

        self.sync_services()
Esempio n. 10
0
    def __init__(self, application, **kwargs):
        width = int(settings.read_setting("width") or self.default_width)
        height = int(settings.read_setting("height") or self.default_height)
        super().__init__(default_width=width,
                         default_height=height,
                         window_position=Gtk.WindowPosition.NONE,
                         icon_name="lutris",
                         application=application,
                         **kwargs)
        self.application = application
        self.runtime_updater = RuntimeUpdater()
        self.threads_stoppers = []
        self.icon_type = None
        self.service = None

        # Load settings
        self.window_size = (width, height)
        self.maximized = settings.read_setting("maximized") == "True"

        icon_type = self.load_icon_type()
        self.service_media = self.get_service_media(icon_type)

        # Window initialization
        self.game_actions = GameActions(application=application, window=self)
        self.search_timer_id = None
        self.game_store = None
        self.view = Gtk.Box()

        self.connect("delete-event", self.on_window_delete)
        self.connect("map-event", self.on_load)
        if self.maximized:
            self.maximize()
        self.init_template()
        self._init_actions()
        self._bind_zoom_adjustment()

        # Set theme to dark if set in the settings
        self.set_dark_theme()
        self.set_viewtype_icon(self.view_type)

        # Add additional widgets
        lutris_icon = Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU)
        lutris_icon.set_margin_right(3)
        self.selected_category = settings.read_setting("selected_category",
                                                       default="runner:all")
        category, value = self.selected_category.split(":")
        self.filters = {
            category: value
        }  # Type of filter corresponding to the selected sidebar element
        self.sidebar = LutrisSidebar(self.application,
                                     selected=self.selected_category)
        self.sidebar.set_size_request(250, -1)
        self.sidebar.connect("selected-rows-changed", self.on_sidebar_changed)
        self.sidebar_scrolled.add(self.sidebar)

        # Sidebar visibility
        self.sidebar_revealer.set_reveal_child(self.left_side_panel_visible)
        self.sidebar_revealer.set_transition_duration(300)

        self.game_bar = None
        self.service_bar = None
        self.revealer_box = Gtk.HBox(visible=True)
        self.game_revealer.add(self.revealer_box)
Esempio n. 11
0
    def __init__(self, application, **kwargs):
        self.application = application
        self.runtime_updater = RuntimeUpdater()
        self.threads_stoppers = []

        # Emulate double click to workaround GTK bug #484640
        # https://bugzilla.gnome.org/show_bug.cgi?id=484640
        self.game_launch_time = 0
        self.selected_runner = None
        self.selected_platform = None
        self.icon_type = None

        # Load settings
        width = int(settings.read_setting("width") or 800)
        height = int(settings.read_setting("height") or 600)
        self.window_size = (width, height)
        self.maximized = settings.read_setting("maximized") == "True"

        view_type = self.get_view_type()
        self.load_icon_type_from_settings(view_type)
        self.filter_installed = settings.read_setting(
            "filter_installed") == "true"
        self.show_installed_first = (
            settings.read_setting("show_installed_first") == "true")
        self.sidebar_visible = settings.read_setting("sidebar_visible") in [
            "true",
            None,
        ]
        self.view_sorting = settings.read_setting("view_sorting") or "name"
        self.view_sorting_ascending = (
            settings.read_setting("view_sorting_ascending") != "false")
        self.use_dark_theme = (settings.read_setting(
            "dark_theme", default="false").lower() == "true")
        self.show_tray_icon = (settings.read_setting(
            "show_tray_icon", default="false").lower() == "true")

        # Window initialization
        self.game_actions = GameActions(application=application, window=self)
        self.game_list = pga.get_games(
            show_installed_first=self.show_installed_first)
        self.game_store = GameStore(
            [],
            self.icon_type,
            self.filter_installed,
            self.view_sorting,
            self.view_sorting_ascending,
            self.show_installed_first,
        )
        self.view = self.get_view(view_type)
        self.game_store.connect("sorting-changed",
                                self.on_game_store_sorting_changed)
        super().__init__(default_width=width,
                         default_height=height,
                         icon_name="lutris",
                         application=application,
                         **kwargs)
        if self.maximized:
            self.maximize()
        self.init_template()
        self._init_actions()
        self._bind_zoom_adjustment()

        # Load view
        self.games_scrollwindow.add(self.view)
        self._connect_signals()
        # Set theme to dark if set in the settings
        self.set_dark_theme(self.use_dark_theme)
        self.set_viewtype_icon(view_type)

        # Add additional widgets
        self.sidebar_listbox = SidebarListBox()
        self.sidebar_listbox.set_size_request(250, -1)
        self.sidebar_listbox.connect("selected-rows-changed",
                                     self.on_sidebar_changed)
        self.sidebar_scrolled.add(self.sidebar_listbox)

        self.game_revealer = Gtk.Revealer()
        self.game_revealer.show()
        self.game_revealer.set_transition_duration(500)
        self.game_revealer.set_transition_type(
            Gtk.RevealerTransitionType.SLIDE_LEFT)

        self.game_scrolled = Gtk.ScrolledWindow()
        self.game_scrolled.set_size_request(320, -1)
        self.game_scrolled.set_policy(Gtk.PolicyType.NEVER,
                                      Gtk.PolicyType.NEVER)
        self.game_scrolled.show()
        self.game_revealer.add(self.game_scrolled)

        self.game_panel = Gtk.Box()
        self.main_box.pack_end(self.game_revealer, False, False, 0)

        self.view.show()

        # Contextual menu
        self.view.contextual_menu = ContextualMenu(
            self.game_actions.get_game_actions())

        # Sidebar
        self.game_store.fill_store(self.game_list)
        self.switch_splash_screen()

        self.sidebar_revealer.set_reveal_child(self.sidebar_visible)
        self.update_runtime()

        # Connect account and/or sync
        credentials = api.read_api_key()
        if credentials:
            self.on_connect_success(None, credentials["username"])
        else:
            self.toggle_connection(False)
            self.sync_library()

        self.sync_services()
Esempio n. 12
0
class LutrisWindow(Gtk.ApplicationWindow):
    """Handler class for main window signals."""

    __gtype_name__ = "LutrisWindow"

    main_box = GtkTemplate.Child()
    splash_box = GtkTemplate.Child()
    connect_link = GtkTemplate.Child()
    games_scrollwindow = GtkTemplate.Child()
    sidebar_revealer = GtkTemplate.Child()
    sidebar_scrolled = GtkTemplate.Child()
    connection_label = GtkTemplate.Child()
    search_revealer = GtkTemplate.Child()
    search_entry = GtkTemplate.Child()
    search_toggle = GtkTemplate.Child()
    zoom_adjustment = GtkTemplate.Child()
    no_results_overlay = GtkTemplate.Child()
    connect_button = GtkTemplate.Child()
    disconnect_button = GtkTemplate.Child()
    register_button = GtkTemplate.Child()
    sync_button = GtkTemplate.Child()
    sync_label = GtkTemplate.Child()
    sync_spinner = GtkTemplate.Child()
    add_popover = GtkTemplate.Child()
    viewtype_icon = GtkTemplate.Child()

    def __init__(self, application, **kwargs):
        self.application = application
        self.runtime_updater = RuntimeUpdater()
        self.threads_stoppers = []

        # Emulate double click to workaround GTK bug #484640
        # https://bugzilla.gnome.org/show_bug.cgi?id=484640
        self.game_launch_time = 0
        self.selected_runner = None
        self.selected_platform = None
        self.icon_type = None

        # Load settings
        width = int(settings.read_setting("width") or 800)
        height = int(settings.read_setting("height") or 600)
        self.window_size = (width, height)
        self.maximized = settings.read_setting("maximized") == "True"

        view_type = self.get_view_type()
        self.load_icon_type_from_settings(view_type)
        self.filter_installed = settings.read_setting(
            "filter_installed") == "true"
        self.show_installed_first = (
            settings.read_setting("show_installed_first") == "true")
        self.sidebar_visible = settings.read_setting("sidebar_visible") in [
            "true",
            None,
        ]
        self.view_sorting = settings.read_setting("view_sorting") or "name"
        self.view_sorting_ascending = (
            settings.read_setting("view_sorting_ascending") != "false")
        self.use_dark_theme = (settings.read_setting(
            "dark_theme", default="false").lower() == "true")
        self.show_tray_icon = (settings.read_setting(
            "show_tray_icon", default="false").lower() == "true")

        # Window initialization
        self.game_actions = GameActions(application=application, window=self)
        self.game_list = pga.get_games(
            show_installed_first=self.show_installed_first)
        self.game_store = GameStore(
            [],
            self.icon_type,
            self.filter_installed,
            self.view_sorting,
            self.view_sorting_ascending,
            self.show_installed_first,
        )
        self.view = self.get_view(view_type)
        self.game_store.connect("sorting-changed",
                                self.on_game_store_sorting_changed)
        super().__init__(default_width=width,
                         default_height=height,
                         icon_name="lutris",
                         application=application,
                         **kwargs)
        if self.maximized:
            self.maximize()
        self.init_template()
        self._init_actions()
        self._bind_zoom_adjustment()

        # Load view
        self.games_scrollwindow.add(self.view)
        self._connect_signals()
        # Set theme to dark if set in the settings
        self.set_dark_theme(self.use_dark_theme)
        self.set_viewtype_icon(view_type)

        # Add additional widgets
        self.sidebar_listbox = SidebarListBox()
        self.sidebar_listbox.set_size_request(250, -1)
        self.sidebar_listbox.connect("selected-rows-changed",
                                     self.on_sidebar_changed)
        self.sidebar_scrolled.add(self.sidebar_listbox)

        self.game_revealer = Gtk.Revealer()
        self.game_revealer.show()
        self.game_revealer.set_transition_duration(500)
        self.game_revealer.set_transition_type(
            Gtk.RevealerTransitionType.SLIDE_LEFT)

        self.game_scrolled = Gtk.ScrolledWindow()
        self.game_scrolled.set_size_request(320, -1)
        self.game_scrolled.set_policy(Gtk.PolicyType.NEVER,
                                      Gtk.PolicyType.NEVER)
        self.game_scrolled.show()
        self.game_revealer.add(self.game_scrolled)

        self.game_panel = Gtk.Box()
        self.main_box.pack_end(self.game_revealer, False, False, 0)

        self.view.show()

        # Contextual menu
        self.view.contextual_menu = ContextualMenu(
            self.game_actions.get_game_actions())

        # Sidebar
        self.game_store.fill_store(self.game_list)
        self.switch_splash_screen()

        self.sidebar_revealer.set_reveal_child(self.sidebar_visible)
        self.update_runtime()

        # Connect account and/or sync
        credentials = api.read_api_key()
        if credentials:
            self.on_connect_success(None, credentials["username"])
        else:
            self.toggle_connection(False)
            self.sync_library()

        self.sync_services()

        # steamapps_paths = steam.get_steamapps_paths(flat=True)
        # self.steam_watcher = SteamWatcher(steamapps_paths, self.on_steam_game_changed)

    def _init_actions(self):
        Action = namedtuple(
            "Action", ("callback", "type", "enabled", "default", "accel"))
        Action.__new__.__defaults__ = (None, None, True, None, None)

        actions = {
            "browse-games":
            Action(lambda *x: open_uri("https://lutris.net/games/")),
            "register-account":
            Action(lambda *x: open_uri("https://lutris.net/user/register/")),
            "disconnect":
            Action(self.on_disconnect),
            "connect":
            Action(self.on_connect),
            "synchronize":
            Action(lambda *x: self.sync_library()),
            "sync-local":
            Action(lambda *x: self.open_sync_dialog()),
            "add-game":
            Action(self.on_add_game_button_clicked),
            "preferences":
            Action(self.on_preferences_activate),
            "manage-runners":
            Action(self.on_manage_runners),
            "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,
            ),
            "toggle-viewtype":
            Action(self.on_toggle_viewtype),
            "icon-type":
            Action(self.on_icontype_state_change,
                   type="s",
                   default=self.icon_type),
            "view-sorting":
            Action(self.on_view_sorting_state_change,
                   type="s",
                   default=self.view_sorting),
            "view-sorting-ascending":
            Action(
                self.on_view_sorting_direction_change,
                type="b",
                default=self.view_sorting_ascending,
            ),
            "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 update_games(self, games):
        """Update games from a list of game IDs"""
        game_ids = [game["id"] for game in self.game_list]
        for game_id in games:
            if game_id not in game_ids:
                self.add_game_to_view(game_id)
            else:
                self.view.set_installed(Game(game_id))

    def sync_services(self):
        """Sync local lutris library with current Steam games and desktop games"""
        def full_sync(syncer_cls):
            syncer = syncer_cls()
            games = syncer.load()
            return syncer.sync(games, full=True)

        def on_sync_complete(response, errors):
            """Callback to update the view on sync complete"""
            if errors:
                logger.error("Sync failed: %s", errors)
            added_games, removed_games = response
            self.update_games(added_games)
            for game_id in removed_games:
                self.remove_game_from_view(game_id)

        for service in get_services_synced_at_startup():
            AsyncCall(full_sync, on_sync_complete, service.SYNCER)

    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)
                self.update_games([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.view.connect("game-installed", self.on_game_installed)
        self.view.connect("game-selected", self.game_selection_changed)

    def _bind_zoom_adjustment(self):
        """Bind the zoom slider to the supported banner sizes"""
        image_sizes = list(IMAGE_SIZES.keys())
        self.zoom_adjustment.props.value = image_sizes.index(self.icon_type)
        self.zoom_adjustment.connect(
            "value-changed",
            lambda adj: self._set_icon_type(image_sizes[int(adj.props.value)]),
        )

    @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 do_key_press_event(self, event):
        if event.keyval == Gdk.KEY_Escape:
            self.search_toggle.set_active(False)
            return Gdk.EVENT_STOP

        # Probably not ideal for non-english, but we want to limit
        # which keys actually start searching
        if (not Gdk.KEY_0 <= event.keyval <= Gdk.KEY_z
                or event.state & Gdk.ModifierType.CONTROL_MASK
                or event.state & Gdk.ModifierType.SHIFT_MASK
                or event.state & Gdk.ModifierType.META_MASK
                or event.state & Gdk.ModifierType.MOD1_MASK
                or self.search_entry.has_focus()):
            return Gtk.ApplicationWindow.do_key_press_event(self, event)

        self.search_toggle.set_active(True)
        self.search_entry.grab_focus()
        return self.search_entry.do_key_press_event(self.search_entry, event)

    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 IMAGE_SIZES.keys():
            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.main_box.show()
            self.games_scrollwindow.show()
        else:
            logger.debug("Showing splash screen")
            self.splash_box.show()
            self.main_box.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 = ContextualMenu(
            self.game_actions.get_game_actions())
        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()

        self.zoom_adjustment.props.value = list(IMAGE_SIZES.keys()).index(
            self.icon_type)

        settings.write_setting("view_type", view_type)

    def set_viewtype_icon(self, view_type):
        self.viewtype_icon.set_from_icon_name(
            "view-%s-symbolic" % "list" if view_type == "grid" else "grid",
            Gtk.IconSize.BUTTON)

    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.sync_label.set_label("Synchronize library")
            self.sync_spinner.props.active = False
            self.sync_button.set_sensitive(True)

        self.sync_label.set_label("Synchronizing…")
        self.sync_spinner.props.active = True
        self.sync_button.set_sensitive(False)
        AsyncCall(sync_from_remote, update_gui)

    def open_sync_dialog(self):
        """Opens the service sync dialog"""
        self.add_popover.hide()
        SyncServiceWindow(application=self.application)

    def update_existing_games(self, added, updated, first_run=False):
        """Updates the games in the view from the callback of the method
        Still, working on this docstring.
        If the implementation is shit,  the docstring is as well.
        """
        for game_id in updated.difference(added):
            game = pga.get_game_by_field(game_id, "id")
            self.view.update_row(game["id"], game["year"], game["playtime"])

        if first_run:
            self.update_games(added)
            game_slugs = [game["slug"] for game in self.game_list]
            AsyncCall(resources.get_missing_media, self.on_media_returned,
                      game_slugs)

    def on_media_returned(self, lutris_media, _error=None):
        """Called when the Lutris API has provided a list of downloadable media"""
        icons_sync = AsyncCall(resources.fetch_icons, None, lutris_media, self)
        self.threads_stoppers.append(icons_sync.stop_request.set)

    def update_runtime(self):
        """Check that the runtime is up to date"""
        runtime_sync = AsyncCall(self.runtime_updater.update, None)
        self.threads_stoppers.append(runtime_sync.stop_request.set)

    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, username):
        """Callback for user connect success"""
        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.connect_button.props.visible = not is_connected
        self.register_button.props.visible = not is_connected
        self.disconnect_button.props.visible = is_connected
        self.sync_button.props.visible = is_connected
        if is_connected:
            self.connection_label.set_text(username)
            logger.info("Connected to lutris.net as %s", username)

    @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

        # 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)

    @GtkTemplate.Callback
    def on_manage_runners(self, *args):
        return RunnersDialog(transient_for=self)

    def invalidate_game_filter(self):
        """Refilter the game view based on current filters"""
        self.game_store.modelfilter.refilter()
        self.game_store.modelsort.clear_cache()
        self.game_store.sort_view(self.view_sorting,
                                  self.view_sorting_ascending)
        self.no_results_overlay.props.visible = len(
            self.game_store.modelfilter) == 0

    def on_show_installed_first_state_change(self, action, value):
        """Callback to handle installed games first toggle"""
        action.set_state(value)
        self.set_show_installed_first_state(value.get_boolean())

    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
        settings.write_setting("show_installed_first",
                               "true" if show_installed_first else "false")
        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
        settings.write_setting("filter_installed",
                               "true" if filter_installed else "false")
        self.game_store.filter_installed = filter_installed
        self.invalidate_game_filter()

    @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 for the search input keypresses"""
        self.game_store.filter_text = widget.get_text()
        self.invalidate_game_filter()

    @GtkTemplate.Callback
    def on_search_toggle(self, button):
        active = button.props.active
        self.search_revealer.set_reveal_child(active)
        if not active:
            self.search_entry.props.text = ""
        else:
            self.search_entry.grab_focus()

    @GtkTemplate.Callback
    def on_about_clicked(self, *_args):
        """Open the about dialog."""
        dialogs.AboutDialog(parent=self)

    def on_game_error(self, game, error):
        """Called when a game has sent the 'game-error' signal"""
        logger.error("%s crashed", game)
        dialogs.ErrorDialog(error, parent=self)

    def game_selection_changed(self, widget):
        """Callback to handle the selection of a game in the view"""
        child = self.game_scrolled.get_child()
        if child:
            self.game_scrolled.remove(child)
            child.destroy()

        if not self.view.selected_game:
            self.game_revealer.set_reveal_child(False)
            return
        self.game_actions.set_game(game_id=self.view.selected_game)
        self.game_panel = GamePanel(self.game_actions)
        self.game_scrolled.add(self.game_panel)
        self.game_revealer.set_reveal_child(True)

    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_listbox.update()
        GLib.idle_add(resources.fetch_icon, game.slug,
                      self.on_image_downloaded)

    def on_image_downloaded(self, game_slugs, _error=None):
        """Callback for handling successful image downloads"""
        for game_slug in game_slugs:
            self.update_image_for_slug(game_slug)

    def update_image_for_slug(self, slug):
        for pga_game in pga.get_games_where(slug=slug):
            game = Game(pga_game["id"])
            self.view.update_image(game.id, game.is_installed)

    @GtkTemplate.Callback
    def on_add_game_button_clicked(self, *_args):
        """Add a new game manually with the AddGameDialog."""
        self.add_popover.hide()
        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_listbox.update()
            return False

        if is_async:
            GLib.idle_add(do_add_game)
        else:
            do_add_game()

    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_listbox.update()

    def on_toggle_viewtype(self, *args):
        if self.current_view_type == "grid":
            self.switch_view("list")
        else:
            self.switch_view("grid")

    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 _set_icon_type(self, icon_type):
        self.icon_type = icon_type
        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 on_icontype_state_change(self, action, value):
        action.set_state(value)
        self._set_icon_type(value.get_string())

    def on_view_sorting_state_change(self, action, value):
        ascending = self.view_sorting_ascending
        self.game_store.sort_view(value.get_string(), ascending)

    def on_view_sorting_direction_change(self, action, value):
        self.game_store.sort_view(self.view_sorting, value.get_boolean())

    def on_game_store_sorting_changed(self, game_store, key, ascending):
        self.view_sorting = key
        self.view_sorting_ascending = ascending
        self.actions["view-sorting"].set_state(GLib.Variant.new_string(key))
        self.actions["view-sorting-ascending"].set_state(
            GLib.Variant.new_boolean(ascending))
        settings.write_setting("view_sorting", self.view_sorting)
        settings.write_setting(
            "view_sorting_ascending",
            "true" if self.view_sorting_ascending else "false")

    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.sidebar_revealer.set_reveal_child(self.sidebar_visible)

    def on_sidebar_changed(self, widget):
        row = widget.get_selected_row()
        if row is None:
            self.set_selected_filter(None, None)
        elif row.type == "runner":
            self.set_selected_filter(row.id, None)
        else:
            self.set_selected_filter(None, row.id)

    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 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.invalidate_game_filter()
Esempio n. 13
0
    def __init__(self, application, **kwargs):
        width = int(settings.read_setting("width") or self.default_width)
        height = int(settings.read_setting("height") or self.default_height)
        super().__init__(
            default_width=width,
            default_height=height,
            icon_name="lutris",
            application=application,
            **kwargs
        )
        self.application = application
        self.runtime_updater = RuntimeUpdater()
        self.threads_stoppers = []
        self.selected_runner = None
        self.selected_platform = None
        self.icon_type = None

        # Load settings
        self.window_size = (width, height)
        self.maximized = settings.read_setting("maximized") == "True"

        view_type = self.get_view_type()
        self.load_icon_type_from_settings(view_type)

        # Window initialization
        self.game_actions = GameActions(application=application, window=self)

        self.search_terms = None
        self.search_timer_id = None
        self.search_mode = "local"
        self.game_store = self.get_store()
        self.view = self.get_view(view_type)

        GObject.add_emission_hook(Game, "game-updated", self.on_game_updated)
        GObject.add_emission_hook(Game, "game-removed", self.on_game_updated)
        GObject.add_emission_hook(GenericPanel,
                                  "running-game-selected",
                                  self.game_selection_changed)
        self.connect("delete-event", self.on_window_delete)
        if self.maximized:
            self.maximize()
        self.init_template()
        self._init_actions()
        self._bind_zoom_adjustment()

        # Load view
        self.games_scrollwindow.add(self.view)
        self._connect_signals()
        # Set theme to dark if set in the settings
        self.set_dark_theme()
        self.set_viewtype_icon(view_type)

        # Add additional widgets
        lutris_icon = Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU)
        lutris_icon.set_margin_right(3)
        self.website_search_toggle.set_image(lutris_icon)
        self.website_search_toggle.set_label("Search Lutris.net")
        self.website_search_toggle.set_tooltip_text("Search Lutris.net")
        self.sidebar_listbox = SidebarListBox()
        self.sidebar_listbox.set_size_request(250, -1)
        self.sidebar_listbox.connect("selected-rows-changed", self.on_sidebar_changed)
        self.sidebar_scrolled.add(self.sidebar_listbox)

        self.game_panel = GenericPanel(application=self.application)

        self.game_scrolled = Gtk.ScrolledWindow(visible=True)
        self.game_scrolled.set_size_request(320, -1)
        self.game_scrolled.get_style_context().add_class("game-scrolled")
        self.game_scrolled.set_policy(Gtk.PolicyType.EXTERNAL, Gtk.PolicyType.EXTERNAL)
        self.game_scrolled.add(self.game_panel)

        self.panel_revealer = Gtk.Revealer(visible=True)
        self.panel_revealer.set_transition_duration(300)
        self.panel_revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_LEFT)
        self.panel_revealer.set_reveal_child(True)
        self.panel_revealer.add(self.game_scrolled)

        self.main_box.pack_end(self.panel_revealer, False, False, 0)

        self.view.show()

        self.game_store.load()
        # Contextual menu
        self.view.contextual_menu = ContextualMenu(self.game_actions.get_game_actions())

        # Sidebar
        self.sidebar_revealer.set_reveal_child(self.sidebar_visible)
        self.sidebar_revealer.set_transition_duration(300)
        self.update_runtime()

        # Connect account and/or sync
        credentials = api.read_api_key()
        if credentials:
            self.on_connect_success(None, credentials["username"])
        else:
            self.toggle_connection(False)
            self.sync_library()

        self.sync_services()
Esempio n. 14
0
class LutrisWindow(Gtk.ApplicationWindow):
    """Handler class for main window signals."""

    default_view_type = "grid"
    default_width = 800
    default_height = 600

    __gtype_name__ = "LutrisWindow"

    main_box = GtkTemplate.Child()
    games_scrollwindow = GtkTemplate.Child()
    sidebar_revealer = GtkTemplate.Child()
    sidebar_scrolled = GtkTemplate.Child()
    connection_label = GtkTemplate.Child()
    search_revealer = GtkTemplate.Child()
    search_entry = GtkTemplate.Child()
    search_toggle = GtkTemplate.Child()
    zoom_adjustment = GtkTemplate.Child()
    no_results_overlay = GtkTemplate.Child()
    connect_button = GtkTemplate.Child()
    disconnect_button = GtkTemplate.Child()
    register_button = GtkTemplate.Child()
    sync_button = GtkTemplate.Child()
    sync_label = GtkTemplate.Child()
    sync_spinner = GtkTemplate.Child()
    add_popover = GtkTemplate.Child()
    viewtype_icon = GtkTemplate.Child()
    website_search_toggle = GtkTemplate.Child()

    def __init__(self, application, **kwargs):
        width = int(settings.read_setting("width") or self.default_width)
        height = int(settings.read_setting("height") or self.default_height)
        super().__init__(
            default_width=width,
            default_height=height,
            icon_name="lutris",
            application=application,
            **kwargs
        )
        self.application = application
        self.runtime_updater = RuntimeUpdater()
        self.threads_stoppers = []
        self.selected_runner = None
        self.selected_platform = None
        self.icon_type = None

        # Load settings
        self.window_size = (width, height)
        self.maximized = settings.read_setting("maximized") == "True"

        view_type = self.get_view_type()
        self.load_icon_type_from_settings(view_type)

        # Window initialization
        self.game_actions = GameActions(application=application, window=self)

        self.search_terms = None
        self.search_timer_id = None
        self.search_mode = "local"
        self.game_store = self.get_store()
        self.view = self.get_view(view_type)

        GObject.add_emission_hook(Game, "game-updated", self.on_game_updated)
        GObject.add_emission_hook(Game, "game-removed", self.on_game_updated)
        GObject.add_emission_hook(GenericPanel,
                                  "running-game-selected",
                                  self.game_selection_changed)
        self.connect("delete-event", self.on_window_delete)
        if self.maximized:
            self.maximize()
        self.init_template()
        self._init_actions()
        self._bind_zoom_adjustment()

        # Load view
        self.games_scrollwindow.add(self.view)
        self._connect_signals()
        # Set theme to dark if set in the settings
        self.set_dark_theme()
        self.set_viewtype_icon(view_type)

        # Add additional widgets
        lutris_icon = Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU)
        lutris_icon.set_margin_right(3)
        self.website_search_toggle.set_image(lutris_icon)
        self.website_search_toggle.set_label("Search Lutris.net")
        self.website_search_toggle.set_tooltip_text("Search Lutris.net")
        self.sidebar_listbox = SidebarListBox()
        self.sidebar_listbox.set_size_request(250, -1)
        self.sidebar_listbox.connect("selected-rows-changed", self.on_sidebar_changed)
        self.sidebar_scrolled.add(self.sidebar_listbox)

        self.game_panel = GenericPanel(application=self.application)

        self.game_scrolled = Gtk.ScrolledWindow(visible=True)
        self.game_scrolled.set_size_request(320, -1)
        self.game_scrolled.get_style_context().add_class("game-scrolled")
        self.game_scrolled.set_policy(Gtk.PolicyType.EXTERNAL, Gtk.PolicyType.EXTERNAL)
        self.game_scrolled.add(self.game_panel)

        self.panel_revealer = Gtk.Revealer(visible=True)
        self.panel_revealer.set_transition_duration(300)
        self.panel_revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_LEFT)
        self.panel_revealer.set_reveal_child(True)
        self.panel_revealer.add(self.game_scrolled)

        self.main_box.pack_end(self.panel_revealer, False, False, 0)

        self.view.show()

        self.game_store.load()
        # Contextual menu
        self.view.contextual_menu = ContextualMenu(self.game_actions.get_game_actions())

        # Sidebar
        self.sidebar_revealer.set_reveal_child(self.sidebar_visible)
        self.sidebar_revealer.set_transition_duration(300)
        self.update_runtime()

        # Connect account and/or sync
        credentials = api.read_api_key()
        if credentials:
            self.on_connect_success(None, credentials["username"])
        else:
            self.toggle_connection(False)
            self.sync_library()

        self.sync_services()

        # steamapps_paths = steam.get_steamapps_paths(flat=True)
        # self.steam_watcher = SteamWatcher(steamapps_paths, self.on_steam_game_changed)

    def _init_actions(self):
        Action = namedtuple(
            "Action", ("callback", "type", "enabled", "default", "accel")
        )
        Action.__new__.__defaults__ = (None, None, True, None, None)

        actions = {
            "browse-games": Action(lambda *x: open_uri("https://lutris.net/games/")),
            "register-account": Action(
                lambda *x: open_uri("https://lutris.net/user/register/")
            ),
            "disconnect": Action(self.on_disconnect),
            "connect": Action(self.on_connect),
            "synchronize": Action(lambda *x: self.sync_library()),
            "sync-local": Action(lambda *x: self.open_sync_dialog()),
            "add-game": Action(self.on_add_game_button_clicked),
            "preferences": Action(self.on_preferences_activate),
            "manage-runners": Action(self.on_manage_runners),
            "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,
            ),
            "toggle-viewtype": Action(self.on_toggle_viewtype),
            "icon-type": Action(
                self.on_icontype_state_change, type="s", default=self.icon_type
            ),
            "view-sorting": Action(
                self.on_view_sorting_state_change, type="s", default=self.view_sorting
            ),
            "view-sorting-ascending": Action(
                self.on_view_sorting_direction_change,
                type="b",
                default=self.view_sorting_ascending,
            ),
            "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",
            ),
            "open-forums": Action(lambda *x: open_uri("https://forums.lutris.net/")),
            "open-discord": Action(lambda *x: open_uri("https://discord.gg/Pnt5CuY")),
            "donate": Action(lambda *x: open_uri("https://lutris.net/donate")),
        }

        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"

    @property
    def filter_installed(self):
        return settings.read_setting("filter_installed") == "true"

    @property
    def sidebar_visible(self):
        return settings.read_setting("sidebar_visible") in [
            "true",
            None,
        ]

    @property
    def use_dark_theme(self):
        """Return whether to use the dark theme variant (if the theme provides one)"""
        return settings.read_setting("dark_theme", default="false").lower() == "true"

    @property
    def show_installed_first(self):
        return settings.read_setting("show_installed_first") == "true"

    @property
    def view_sorting(self):
        return settings.read_setting("view_sorting") or "name"

    @property
    def view_sorting_ascending(self):
        return settings.read_setting("view_sorting_ascending") != "false"

    def get_store(self, games=None):
        """Return an instance of GameStore"""
        games = games or pga.get_games(show_installed_first=self.show_installed_first)
        game_store = GameStore(
            games,
            self.icon_type,
            self.filter_installed,
            self.view_sorting,
            self.view_sorting_ascending,
            self.show_installed_first,
        )
        game_store.connect("sorting-changed", self.on_game_store_sorting_changed)
        return game_store

    def sync_services(self):
        """Sync local lutris library with current Steam games and desktop games"""
        def full_sync(syncer_cls):
            syncer = syncer_cls()
            games = syncer.load()
            return syncer.sync(games, full=True)

        def on_sync_complete(response, errors):
            """Callback to update the view on sync complete"""
            if errors:
                logger.error("Sync failed: %s", errors)
                return
            added_games, removed_games = response

            for game_id in added_games:
                self.game_store.add_or_update(game_id)

            for game_id in removed_games:
                self.remove_game_from_view(game_id)

        for service in get_services_synced_at_startup():
            AsyncCall(full_sync, on_sync_complete, service.SYNCER)

    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.game_store.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
                )
                self.game_store.update_game_by_id(game_id)

    def set_dark_theme(self):
        """Enables or disbales dark theme"""
        gtksettings = Gtk.Settings.get_default()
        gtksettings.set_property("gtk-application-prefer-dark-theme", self.use_dark_theme)

    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.view.connect("game-selected", self.game_selection_changed)
        self.view.connect("game-activated", self.on_game_activated)

    def _bind_zoom_adjustment(self):
        """Bind the zoom slider to the supported banner sizes"""
        image_sizes = list(IMAGE_SIZES.keys())
        self.zoom_adjustment.props.value = image_sizes.index(self.icon_type)
        self.zoom_adjustment.connect(
            "value-changed",
            lambda adj: self._set_icon_type(image_sizes[int(adj.props.value)]),
        )

    @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)

    def get_view_type(self):
        """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 self.default_view_type

    def do_key_press_event(self, event):
        if event.keyval == Gdk.KEY_Escape:
            self.search_toggle.set_active(False)
            return Gdk.EVENT_STOP
        # return Gtk.ApplicationWindow.do_key_press_event(self, event)

        # XXX: This block of code below is to enable searching on type.
        # Enabling this feature steals focus from other entries so it needs
        # some kind of focus detection before enabling library search.

        # Probably not ideal for non-english, but we want to limit
        # which keys actually start searching
        if (
            not Gdk.KEY_0 <= event.keyval <= Gdk.KEY_z
            or event.state & Gdk.ModifierType.CONTROL_MASK
            or event.state & Gdk.ModifierType.SHIFT_MASK
            or event.state & Gdk.ModifierType.META_MASK
            or event.state & Gdk.ModifierType.MOD1_MASK
            or self.search_entry.has_focus()
        ):
            return Gtk.ApplicationWindow.do_key_press_event(self, event)

        self.search_toggle.set_active(True)
        self.search_entry.grab_focus()
        return self.search_entry.do_key_press_event(self.search_entry, event)

    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 = "icon"
        else:
            self.icon_type = settings.read_setting("icon_type_gridview")
            default = "banner"
        if self.icon_type not in IMAGE_SIZES.keys():
            self.icon_type = default
        return self.icon_type

    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 = ContextualMenu(self.game_actions.get_game_actions())
        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()

        self.zoom_adjustment.props.value = list(IMAGE_SIZES.keys()).index(self.icon_type)

        self.set_viewtype_icon(view_type)
        settings.write_setting("view_type", view_type)

    def set_viewtype_icon(self, view_type):
        self.viewtype_icon.set_from_icon_name(
            "view-%s-symbolic" % ("list" if view_type == "grid" else "grid"),
            Gtk.IconSize.BUTTON
        )

    def sync_library(self):
        """Synchronize games with local stuff and server."""

        def update_gui(result, error):
            self.sync_label.set_label("Synchronize library")
            self.sync_spinner.props.active = False
            self.sync_button.set_sensitive(True)
            if error:
                if isinstance(error, http.UnauthorizedAccess):
                    GLib.idle_add(self.show_invalid_credential_warning)
                else:
                    GLib.idle_add(self.show_library_sync_error)
                return
            if result:
                added_ids, updated_ids = result
                self.game_store.add_games_by_ids(added_ids)
                for game_id in updated_ids.difference(added_ids):
                    self.game_store.update_game_by_id(game_id)
            else:
                logger.error("No results returned when syncing the library")

        self.sync_label.set_label("Synchronizing…")
        self.sync_spinner.props.active = True
        self.sync_button.set_sensitive(False)
        AsyncCall(sync_from_remote, update_gui)

    def open_sync_dialog(self):
        """Opens the service sync dialog"""
        self.add_popover.hide()
        SyncServiceWindow(application=self.application)

    def update_runtime(self):
        """Check that the runtime is up to date"""
        runtime_sync = AsyncCall(self.runtime_updater.update, None)
        self.threads_stoppers.append(runtime_sync.stop_request.set)

    def on_dark_theme_state_change(self, action, value):
        """Callback for theme switching action"""
        action.set_state(value)
        settings.write_setting("dark_theme", "true" if value.get_boolean() else "false")
        self.set_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, username):
        """Callback for user connect success"""
        self.toggle_connection(True, username)
        self.sync_library()
        self.actions["synchronize"].props.enabled = True
        self.actions["register-account"].props.enabled = False

    def on_game_activated(self, _widget, game):
        self.game_selection_changed(None, game)
        if game.is_installed:
            self.application.launch(game)
        else:
            InstallerWindow(
                parent=self,
                game_slug=game.slug,
                application=self.application,
            )

    @GtkTemplate.Callback
    def on_disconnect(self, *_args):
        """Callback from user disconnect"""
        dlg = dialogs.QuestionDialog(
            {
                "question": "Do you want to log out from Lutris?",
                "title": "Log out?",
            }
        )
        if dlg.result != Gtk.ResponseType.YES:
            return
        api.disconnect()
        self.toggle_connection(False)
        self.actions["synchronize"].props.enabled = False

    def toggle_connection(self, is_connected, username=None):
        """Sets or unset connected state for the current user"""
        self.connect_button.props.visible = not is_connected
        self.register_button.props.visible = not is_connected
        self.disconnect_button.props.visible = is_connected
        self.sync_button.props.visible = is_connected
        if is_connected:
            self.connection_label.set_text(username)
            logger.info("Connected to lutris.net as %s", username)

    @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()

    def on_window_delete(self, *_args):
        if self.application.running_games.get_n_items():
            dlg = dialogs.QuestionDialog(
                {
                    "question": ("Some games are still running. "
                                 "Are you sure you want to quit Lutris?"),
                    "title": "Quit Lutris?",
                }
            )
            if dlg.result != Gtk.ResponseType.YES:
                return True

    @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

        # 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)

    @GtkTemplate.Callback
    def on_manage_runners(self, *args):
        return RunnersDialog(transient_for=self)

    def invalidate_game_filter(self):
        """Refilter the game view based on current filters"""
        self.game_store.modelfilter.refilter()
        self.game_store.modelsort.clear_cache()
        self.game_store.sort_view(self.view_sorting, self.view_sorting_ascending)
        self.no_results_overlay.props.visible = not bool(self.game_store.games)

    def on_show_installed_first_state_change(self, action, value):
        """Callback to handle installed games first toggle"""
        action.set_state(value)
        self.set_show_installed_first_state(value.get_boolean())

    def set_show_installed_first_state(self, show_installed_first):
        """Shows the installed games first in the view"""
        settings.write_setting("show_installed_first", "true" if show_installed_first else "false")
        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)
        self.set_show_installed_state(value.get_boolean())

    def set_show_installed_state(self, filter_installed):
        """Shows or hide uninstalled games"""
        settings.write_setting("filter_installed", "true" if filter_installed else "false")
        self.game_store.filter_installed = filter_installed
        self.invalidate_game_filter()

    @GtkTemplate.Callback
    def on_search_entry_changed(self, entry):
        """Callback for the search input keypresses"""
        if self.search_mode == "local":
            self.game_store.filter_text = entry.get_text()
            self.invalidate_game_filter()
        elif self.search_mode == "website":
            if self.search_timer_id:
                GLib.source_remove(self.search_timer_id)
            self.search_timer_id = GLib.timeout_add(
                750, self.on_search_games_fire, entry.get_text().lower().strip()
            )
        else:
            raise ValueError("Unsupported search mode %s" % self.search_mode)

    @GtkTemplate.Callback
    def on_search_toggle(self, button):
        """Called when search bar is shown / hidden"""
        active = button.props.active
        self.search_revealer.set_reveal_child(active)
        if active:
            self.search_entry.grab_focus()
        else:
            self.search_entry.props.text = ""

    @GtkTemplate.Callback
    def on_website_search_toggle_toggled(self, toggle_button):
        self.search_terms = self.search_entry.props.text
        if toggle_button.props.active:
            self.search_mode = "website"
            self.search_entry.set_placeholder_text("Search Lutris.net")
            self.search_entry.set_icon_from_icon_name(
                Gtk.EntryIconPosition.PRIMARY,
                "folder-download-symbolic"
            )
            self.game_store.search_mode = True
            self.search_games(self.search_terms)
        else:
            self.search_mode = "local"
            self.search_entry.set_placeholder_text("Filter the list of games")
            self.search_entry.set_icon_from_icon_name(
                Gtk.EntryIconPosition.PRIMARY,
                "system-search-symbolic"
            )
            self.search_games("")

    @GtkTemplate.Callback
    def on_about_clicked(self, *_args):
        """Open the about dialog."""
        dialogs.AboutDialog(parent=self)

    def on_game_error(self, game, error):
        """Called when a game has sent the 'game-error' signal"""
        logger.error("%s crashed", game)
        dialogs.ErrorDialog(error, parent=self)

    def on_game_updated(self, game):
        """Callback to refresh the view when a game is updated"""
        logger.debug("Updating game %s", game)
        game.load_config()
        try:
            self.game_store.update_game_by_id(game.id)
        except ValueError:
            self.game_store.add_game_by_id(game.id)

        self.view.set_selected_game(game.id)
        self.game_selection_changed(None, game)
        return True

    def on_search_games_fire(self, value):
        GLib.source_remove(self.search_timer_id)
        self.search_timer_id = None
        self.search_games(value)
        return False

    def search_games(self, query):
        """Search for games from the website API"""
        logger.debug("%s search for :%s", self.search_mode, query)
        self.search_terms = query
        self.view.destroy()
        self.game_store = self.get_store(api.search_games(query) if query else None)
        self.game_store.set_icon_type(self.icon_type)
        self.game_store.load(from_search=bool(query))
        self.game_store.filter_text = self.search_entry.props.text
        self.switch_view(self.get_view_type())
        self.invalidate_game_filter()

    def game_selection_changed(self, _widget, game):
        """Callback to handle the selection of a game in the view"""
        child = self.game_scrolled.get_child()
        if child:
            self.game_scrolled.remove(child)
            child.destroy()

        if not game:
            self.game_panel = GenericPanel(application=self.application)
        else:
            self.game_actions.set_game(game=game)
            self.game_panel = GamePanel(self.game_actions)
            self.game_panel.connect("panel-closed", self.on_panel_closed)
            self.view.contextual_menu.connect("shortcut-edited", self.game_panel.on_shortcut_edited)
        self.game_scrolled.add(self.game_panel)
        return True

    def on_panel_closed(self, panel):
        self.game_selection_changed(panel, None)

    def update_game(self, slug):
        for pga_game in pga.get_games_where(slug=slug):
            self.game_store.update(pga_game)

    @GtkTemplate.Callback
    def on_add_game_button_clicked(self, *_args):
        """Add a new game manually with the AddGameDialog."""
        self.add_popover.hide()
        AddGameDialog(self, runner=self.selected_runner)
        return True

    def remove_game_from_view(self, game_id, from_library=False):
        """Remove a game from the view"""
        self.game_store.update_game_by_id(game_id)
        self.sidebar_listbox.update()

    def on_toggle_viewtype(self, *args):
        self.switch_view("list" if self.current_view_type == "grid" else "grid")

    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 _set_icon_type(self, icon_type):
        self.icon_type = icon_type
        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 on_icontype_state_change(self, action, value):
        action.set_state(value)
        self._set_icon_type(value.get_string())

    def on_view_sorting_state_change(self, action, value):
        self.game_store.sort_view(value.get_string(), self.view_sorting_ascending)

    def on_view_sorting_direction_change(self, action, value):
        self.game_store.sort_view(self.view_sorting, value.get_boolean())

    def on_game_store_sorting_changed(self, _game_store, key, ascending):
        self.actions["view-sorting"].set_state(GLib.Variant.new_string(key))
        settings.write_setting("view_sorting", key)

        self.actions["view-sorting-ascending"].set_state(GLib.Variant.new_boolean(ascending))
        settings.write_setting("view_sorting_ascending", "true" if ascending else "false")

    def on_sidebar_state_change(self, action, value):
        """Callback to handle siderbar toggle"""
        action.set_state(value)
        sidebar_visible = value.get_boolean()
        settings.write_setting("sidebar_visible", "true" if sidebar_visible else "false")
        self.sidebar_revealer.set_reveal_child(sidebar_visible)
        self.panel_revealer.set_reveal_child(sidebar_visible)
        self.game_scrolled.set_visible(sidebar_visible)

    def on_sidebar_changed(self, widget):
        row = widget.get_selected_row()
        if row is None:
            self.set_selected_filter(None, None)
        elif row.type == "runner":
            self.set_selected_filter(row.id, None)
        else:
            self.set_selected_filter(None, row.id)

    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.invalidate_game_filter()

    def show_invalid_credential_warning(self):
        dialogs.ErrorDialog("Could not connect to your Lutris account. Please sign in again.")

    def show_library_sync_error(self):
        dialogs.ErrorDialog("Failed to retrieve game library. "
                            "There might be some problems contacting lutris.net")