Ejemplo n.º 1
0
 def on_browse_files(self, widget):
     game = Game(self.view.selected_game)
     path = game.get_browse_dir()
     if path and os.path.exists(path):
         Gtk.show_uri(None, "file://" + path, Gdk.CURRENT_TIME)
     else:
         dialogs.NoticeDialog("Can't open %s \nThe folder doesn't exist." % path)
Ejemplo n.º 2
0
 def on_browse_files(self, widget):
     game = Game(self.view.selected_game)
     path = game.get_browse_dir()
     if path and os.path.exists(path):
         subprocess.Popen(['xdg-open', path])
     else:
         dialogs.NoticeDialog(
         "Can't open %s \nThe folder doesn't exist." % path)
Ejemplo n.º 3
0
def update_platforms():
    pga_games = pga.get_games(filter_installed=True)
    for pga_game in pga_games:
        if pga_game.get('platform') or not pga_game['runner']:
            continue
        game = Game(id=pga_game['id'])
        game.set_platform_from_runner()
        game.save()
Ejemplo n.º 4
0
 def on_browse_files(self, widget):
     game = Game(self.view.selected_game)
     path = game.get_browse_dir()
     if path and os.path.exists(path):
         system.xdg_open(path)
     else:
         dialogs.NoticeDialog(
             "Can't open %s \nThe folder doesn't exist." % path
         )
Ejemplo n.º 5
0
 def launch_game(self, widget, _data=None):
     """Launch a game after it's been installed."""
     widget.set_sensitive(False)
     self.close(widget)
     if self.parent:
         self.parent.on_game_run(game_id=self.interpreter.game_id)
     else:
         game = Game(self.interpreter.game_id)
         game.play()
Ejemplo n.º 6
0
 def platform(self):
     """Platform"""
     _platform = self._pga_data["platform"]
     if not _platform and self.installed:
         game_inst = Game(self._pga_data["id"])
         if game_inst.platform:
             _platform = game_inst.platform
         else:
             logger.debug("Game %s has no platform", self)
             game_inst.set_platform_from_runner()
             _platform = game_inst.platform
     return _platform
Ejemplo n.º 7
0
 def build_game_tab(self):
     if self.game and self.runner_name:
         self.game.runner_name = self.runner_name
         self.game_box = GameBox(self.lutris_config, self.game)
         game_sw = self.build_scrolled_window(self.game_box)
     elif self.runner_name:
         game = Game(None)
         game.runner_name = self.runner_name
         self.game_box = GameBox(self.lutris_config, game)
         game_sw = self.build_scrolled_window(self.game_box)
     else:
         game_sw = Gtk.Label(label=self.no_runner_label)
     self.add_notebook_tab(game_sw, "Game options")
Ejemplo n.º 8
0
def fill_missing_platforms():
    """Sets the platform on games where it's missing.
    This should never happen.
    """
    pga_games = pga.get_games(filter_installed=True)
    for pga_game in pga_games:
        if pga_game.get("platform") or not pga_game["runner"]:
            continue
        game = Game(game_id=pga_game["id"])
        logger.error("Providing missing platorm for game %s", game.slug)
        game.set_platform_from_runner()
        if game.platform:
            game.save(metadata_only=True)
Ejemplo n.º 9
0
    def on_apply_button_clicked(self, widget):
        widget.set_sensitive(False)

        remove_from_library_button = self.builder.get_object(
            'remove_from_library_button'
        )
        remove_from_library = remove_from_library_button.get_active()
        remove_contents_button = self.builder.get_object(
            'remove_contents_button'
        )
        remove_contents = remove_contents_button.get_active()
        game = Game(self.game_slug)
        game.remove(remove_from_library, remove_contents)
        self.callback(self.game_slug, remove_from_library)

        self.on_close()
Ejemplo n.º 10
0
 def game(self):
     if not self._game:
         self._game = self.application.get_game_by_id(self.game_id)
         if not self._game:
             self._game = Game(self.game_id)
         self._game.connect("game-error", self.window.on_game_error)
     return self._game
Ejemplo n.º 11
0
    def initialize(self, slug=None, callback=None):
        self.game = Game(slug)
        self.callback = callback

        self.substitute_label(self.builder.get_object('description_label'),
                              'game', self.game.name)

        self.substitute_label(
            self.builder.get_object('remove_from_library_button'),
            'game', self.game.name
        )
        remove_contents_button = self.builder.get_object(
            'remove_contents_button'
        )
        try:
            default_path = self.game.runner.default_path
        except AttributeError:
            default_path = "/"
        if not is_removeable(self.game.directory, excludes=[default_path]):
            remove_contents_button.set_sensitive(False)
        path = self.game.directory or 'disk'
        self.substitute_label(remove_contents_button, 'path', path)

        cancel_button = self.builder.get_object('cancel_button')
        cancel_button.connect('clicked', self.on_close)

        apply_button = self.builder.get_object('apply_button')
        apply_button.connect('clicked', self.on_apply_button_clicked)
Ejemplo n.º 12
0
    def on_save(self, _button, callback=None):
        """Save game info and destroy widget. Return True if success."""
        if not self.is_valid():
            return False
        name = self.name_entry.get_text()

        # Do not modify slug
        if not self.slug:
            self.slug = slugify(name)

        if not self.game:
            self.game = Game()

        if self.lutris_config.game_config_id == TEMP_CONFIG:
            self.lutris_config.game_config_id = self.get_config_id()

        runner_class = runners.import_runner(self.runner_name)
        runner = runner_class(self.lutris_config)
        self.game.name = name
        self.game.slug = self.slug
        self.game.runner_name = self.runner_name
        self.game.config = self.lutris_config
        self.game.directory = runner.game_path
        self.game.is_installed = True
        if self.runner_name in ('steam', 'winesteam'):
            self.game.steamid = self.lutris_config.game_config['appid']
        self.game.save()
        self.destroy()
        logger.debug("Saved %s", name)
        self.saved = True
        if callback:
            callback()
Ejemplo n.º 13
0
    def on_save(self, _button):
        """Save game info and destroy widget. Return True if success."""
        if not self.is_valid():
            return False
        name = self.name_entry.get_text()

        # Do not modify slug
        if not self.slug:
            self.slug = slugify(name)

        if not self.game:
            self.game = Game(self.slug)
            self.game.config = self.lutris_config

        if not self.lutris_config.game_slug:
            self.lutris_config.game_slug = self.slug
        self.lutris_config.save()

        runner_class = runners.import_runner(self.runner_name)
        runner = runner_class(self.lutris_config)
        self.game.name = name
        self.game.slug = self.slug
        self.game.runner_name = self.runner_name
        self.game.directory = runner.game_path
        self.game.is_installed = True
        self.game.save()
        self.destroy()
        logger.debug("Saved %s", name)
        self.saved = True
Ejemplo n.º 14
0
 def _build_game_tab(self):
     if self.game and self.runner_name:
         self.game.runner_name = self.runner_name
         try:
             self.game.runner = runners.import_runner(self.runner_name)
         except runners.InvalidRunner:
             pass
         self.game_box = GameBox(self.lutris_config, self.game)
         game_sw = self.build_scrolled_window(self.game_box)
     elif self.runner_name:
         game = Game(None)
         game.runner_name = self.runner_name
         self.game_box = GameBox(self.lutris_config, game)
         game_sw = self.build_scrolled_window(self.game_box)
     else:
         game_sw = Gtk.Label(label=self.no_runner_label)
     self._add_notebook_tab(game_sw, "Game options")
Ejemplo n.º 15
0
 def on_game_clicked(self, *args):
     """Launch a game"""
     game_slug = self.view.selected_game
     if game_slug:
         self.running_game = Game(game_slug)
         if self.running_game.is_installed:
             self.running_game.play()
         else:
             InstallerDialog(game_slug, self)
Ejemplo n.º 16
0
    def add_game(self, game):
        name = game['name'].replace('&', "&")
        runner = None
        platform = ''
        runner_name = game['runner']
        runner_human_name = ''
        if runner_name:
            game_inst = Game(game['id'])
            if not game_inst.is_installed:
                return
            if runner_name in self.runner_names:
                runner_human_name = self.runner_names[runner_name]
            else:
                try:
                    runner = runners.import_runner(runner_name)
                except runners.InvalidRunner:
                    game['installed'] = False
                else:
                    runner_human_name = runner.human_name
                    self.runner_names[runner_name] = runner_human_name
            platform = game_inst.platform
            if not platform:
                game_inst.set_platform_from_runner()
                platform = game_inst.platform

        lastplayed = ''
        if game['lastplayed']:
            lastplayed = time.strftime("%c", time.localtime(game['lastplayed']))

        pixbuf = get_pixbuf_for_game(game['slug'], self.icon_type,
                                     game['installed'])
        self.store.append((
            game['id'],
            game['slug'],
            name,
            pixbuf,
            str(game['year'] or ''),
            runner_name,
            runner_human_name,
            platform,
            game['lastplayed'],
            lastplayed,
            game['installed']
        ))
Ejemplo n.º 17
0
 def on_game_clicked(self, *args):
     """Launch a game."""
     game_slug = self.view.selected_game
     if game_slug:
         self.running_game = Game(game_slug)
         if self.running_game.is_installed:
             self.stop_button.set_sensitive(True)
             self.running_game.play()
         else:
             InstallerDialog(game_slug, self)
Ejemplo n.º 18
0
 def on_game_clicked(self, *args):
     """Launch a game, or install it if it is not"""
     game_slug = self._get_current_game_slug()
     if not game_slug:
         return
     self.running_game = Game(game_slug)
     if self.running_game.is_installed:
         self.stop_button.set_sensitive(True)
         self.running_game.play()
     else:
         InstallerDialog(game_slug, self)
Ejemplo n.º 19
0
    def test_can_add_game(self):
        name_entry = self.dlg.name_entry
        name_entry.set_text("Test game")
        self.dlg.runner_dropdown.set_active(1)

        game_box = self.get_game_box()
        exe_box = game_box.get_children()[0].get_children()[0]
        exe_label = exe_box.get_children()[0]
        self.assertEqual(exe_label.get_text(), "Executable")
        test_exe = os.path.abspath(__file__)
        exe_field = exe_box.get_children()[1]
        exe_field.set_file(Gio.File.new_for_path(test_exe))
        exe_field.emit('file-set')
        self.assertEqual(exe_field.get_filename(), test_exe)

        add_button = self.get_buttons().get_children()[1]
        add_button.clicked()

        game = Game('test-game')
        self.assertEqual(game.name, 'Test game')
        game.remove(from_library=True)
Ejemplo n.º 20
0
    def test_can_add_game(self):
        name_entry = self.dlg.name_entry
        name_entry.set_text("Test game")
        self.dlg.runner_dropdown.set_active_id('linux')

        game_box = self.get_game_box()
        exe_box = game_box.get_children()[0].get_children()[0]
        exe_label = exe_box.get_children()[0]
        self.assertEqual(exe_label.get_text(), "Executable")
        test_exe = os.path.abspath(__file__)
        exe_field = exe_box.get_children()[1]
        exe_field.entry.set_text(test_exe)
        self.assertEqual(exe_field.get_text(), test_exe)

        add_button = self.get_buttons().get_children()[1]
        add_button.clicked()

        pga_game = pga.get_game_by_field('test-game', 'slug')
        self.assertTrue(pga_game)
        game = Game(pga_game['id'])
        self.assertEqual(game.name, 'Test game')
        game.remove(from_library=True)
Ejemplo n.º 21
0
 def on_game_run(self, _widget=None, game_id=None):
     """Launch a game, or install it if it is not"""
     if not game_id:
         game_id = self._get_current_game_id()
     if not game_id:
         return
     self.running_game = Game(game_id)
     if self.running_game.is_installed:
         self.running_game.play()
     else:
         game_slug = self.running_game.slug
         self.running_game = None
         InstallerDialog(game_slug, self)
Ejemplo n.º 22
0
    def on_game_clicked(self, *args):
        """Launch a game."""
        # Wait two seconds to avoid running a game twice
        if time.time() - self.game_launch_time < 2:
            return
        self.game_launch_time = time.time()

        game_slug = self.view.selected_game
        if game_slug:
            self.running_game = Game(game_slug)
            if self.running_game.is_installed:
                self.stop_button.set_sensitive(True)
                self.running_game.play()
            else:
                InstallerDialog(game_slug, self)
Ejemplo n.º 23
0
    def initialize(self, game_id=None, callback=None):
        self.game = Game(game_id)
        self.callback = callback
        runner = self.game.runner

        self.substitute_label(self.builder.get_object('description_label'),
                              'game', self.game.name)

        self.substitute_label(
            self.builder.get_object('remove_from_library_button'),
            'game', self.game.name
        )
        remove_contents_button = self.builder.get_object(
            'remove_contents_button'
        )
        if self.game.is_installed:
            path = self.game.directory or ''
            if hasattr(runner, 'own_game_remove_method'):
                remove_contents_button.set_label(runner.own_game_remove_method)
                remove_contents_button.set_active(True)
            else:
                try:
                    default_path = runner.default_path
                except AttributeError:
                    default_path = "/"
                if is_removeable(path, excludes=[default_path]):
                    remove_contents_button.set_active(True)
                else:
                    remove_contents_button.set_sensitive(False)
                    path = 'No game folder'

            path = reverse_expanduser(path)
            self.substitute_label(remove_contents_button, 'path', path)
            label = remove_contents_button.get_child()
            label.set_use_markup(True)
            label.set_line_wrap(True)
            label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
        else:
            remove_contents_button.hide()

        cancel_button = self.builder.get_object('cancel_button')
        cancel_button.connect('clicked', self.on_close)

        apply_button = self.builder.get_object('apply_button')
        apply_button.connect('clicked', self.on_apply_button_clicked)
Ejemplo n.º 24
0
    def initialize(self, slug=None, callback=None):
        self.game = Game(slug)
        self.callback = callback
        runner = self.game.runner

        self.substitute_label(self.builder.get_object('description_label'),
                              'game', self.game.name)

        self.substitute_label(
            self.builder.get_object('remove_from_library_button'),
            'game', self.game.name
        )
        remove_contents_button = self.builder.get_object(
            'remove_contents_button'
        )
        if self.game.is_installed:
            if hasattr(runner, 'own_game_remove_method'):
                remove_contents_button.set_label(runner.own_game_remove_method)
            else:
                try:
                    default_path = runner.default_path
                except AttributeError:
                    default_path = "/"
                try:
                    game_path = runner.game_path
                except AttributeError:
                    game_path = '/'
                if not is_removeable(game_path, excludes=[default_path]):
                    remove_contents_button.set_sensitive(False)

            path = self.game.directory or 'disk'
            self.substitute_label(remove_contents_button, 'path', path)
            remove_contents_button.get_children()[0].set_use_markup(True)
        else:
            remove_contents_button.hide()

        cancel_button = self.builder.get_object('cancel_button')
        cancel_button.connect('clicked', self.on_close)

        apply_button = self.builder.get_object('apply_button')
        apply_button.connect('clicked', self.on_apply_button_clicked)
Ejemplo n.º 25
0
class UninstallGameDialog(GtkBuilderDialog):
    glade_file = "dialog-uninstall-game.ui"
    dialog_object = "uninstall-game-dialog"

    @staticmethod
    def substitute_label(widget, name, replacement):
        if hasattr(widget, "get_text"):
            get_text = widget.get_text
            set_text = widget.set_text
        elif hasattr(widget, "get_label"):
            get_text = widget.get_label
            set_text = widget.set_label
        else:
            raise TypeError("Unsupported type %s" % type(widget))

        set_text(get_text().replace("{%s}" % name, replacement))

    def initialize(self, game_id=None, callback=None):
        self.game = Game(game_id)
        self.callback = callback
        runner = self.game.runner

        self.substitute_label(self.builder.get_object("description_label"),
                              "game", self.game.name)

        self.substitute_label(
            self.builder.get_object("remove_from_library_button"),
            "game",
            self.game.name,
        )
        remove_contents_button = self.builder.get_object(
            "remove_contents_button")
        if self.game.is_installed:
            path = self.game.directory or ""
            if hasattr(runner, "own_game_remove_method"):
                remove_contents_button.set_label(runner.own_game_remove_method)
                remove_contents_button.set_active(True)
            else:
                try:
                    default_path = runner.default_path
                except AttributeError:
                    default_path = "/"
                if is_removeable(path, excludes=[default_path]):
                    remove_contents_button.set_active(True)
                else:
                    remove_contents_button.set_sensitive(False)
                    path = _("No game folder")

            path = reverse_expanduser(path)
            self.substitute_label(remove_contents_button, "path", path)
            label = remove_contents_button.get_child()
            label.set_use_markup(True)
            label.set_line_wrap(True)
            label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
        else:
            remove_contents_button.hide()

        cancel_button = self.builder.get_object("cancel_button")
        cancel_button.connect("clicked", self.on_close)

        apply_button = self.builder.get_object("apply_button")
        apply_button.connect("clicked", self.on_apply_button_clicked)

    def on_apply_button_clicked(self, widget):
        widget.set_sensitive(False)

        remove_from_library_button = self.builder.get_object(
            "remove_from_library_button")
        remove_from_library = remove_from_library_button.get_active()
        remove_contents_button = self.builder.get_object(
            "remove_contents_button")
        remove_contents = remove_contents_button.get_active()
        if remove_contents and not hasattr(self.game.runner,
                                           "no_game_remove_warning"):
            game_dir = self.game.directory.replace("&", "&amp;")
            dlg = QuestionDialog({
                "question":
                _("Are you sure you want to delete EVERYTHING under "
                  "\n<b>%s</b>?\n (This can't be undone)") % game_dir,
                "title":
                _("CONFIRM DANGEROUS OPERATION"),
            })
            if dlg.result != Gtk.ResponseType.YES:
                widget.set_sensitive(True)
                return

        remove_from_library = self.game.remove(remove_from_library,
                                               remove_contents)
        self.callback(self.game.id, remove_from_library)

        self.on_close()
Ejemplo n.º 26
0
 def on_game_selected(self, _widget, game_id):
     Game(game_id).launch()
Ejemplo n.º 27
0
 def on_image_downloaded(self, game_slug):
     is_installed = Game(game_slug).is_installed
     self.view.update_image(game_slug, is_installed)
Ejemplo n.º 28
0
 def remove_desktop_shortcut(self, *args):
     game = Game(self.view.selected_game)
     xdg.remove_launcher(game.slug, game.id, desktop=True)
Ejemplo n.º 29
0
class GameActions:
    """Regroup a list of callbacks for a game"""
    def __init__(self, application=None, window=None):
        self.application = application or Gio.Application.get_default()
        self.window = window
        self.game_id = None
        self._game = None

    @property
    def game(self):
        if not self._game:
            self._game = self.application.get_game_by_id(self.game_id)
            if not self._game:
                self._game = Game(self.game_id)
            self._game.connect("game-error", self.window.on_game_error)
        return self._game

    @property
    def is_game_running(self):
        return bool(self.application.get_game_by_id(self.game_id))

    def set_game(self, game=None, game_id=None):
        if game:
            self._game = game
            self.game_id = game.id
        else:
            self._game = None
            self.game_id = game_id

    def get_game_actions(self):
        """Return a list of game actions and their callbacks"""
        return [
            ("play", _("Play"), self.on_game_launch),
            ("stop", _("Stop"), self.on_game_stop),
            ("show_logs", _("Show logs"), self.on_show_logs),
            ("install", _("Install"), self.on_install_clicked),
            ("add", _("Add installed game"), self.on_add_manually),
            ("configure", _("Configure"), self.on_edit_game_configuration),
            ("favorite", _("Add to favorites"), self.on_add_favorite_game),
            ("deletefavorite", _("Remove from favorites"),
             self.on_delete_favorite_game),
            ("execute-script", _("Execute script"),
             self.on_execute_script_clicked),
            ("browse", _("Browse files"), self.on_browse_files),
            (
                "desktop-shortcut",
                _("Create desktop shortcut"),
                self.on_create_desktop_shortcut,
            ),
            (
                "rm-desktop-shortcut",
                _("Delete desktop shortcut"),
                self.on_remove_desktop_shortcut,
            ),
            (
                "menu-shortcut",
                _("Create application menu shortcut"),
                self.on_create_menu_shortcut,
            ),
            (
                "rm-menu-shortcut",
                _("Delete application menu shortcut"),
                self.on_remove_menu_shortcut,
            ),
            ("install_more", _("Install another version"),
             self.on_install_clicked),
            ("remove", _("Remove"), self.on_remove_game),
            ("view", _("View on Lutris.net"), self.on_view_game),
            ("hide", _("Hide game from library"), self.on_hide_game),
            ("unhide", _("Unhide game from library"), self.on_unhide_game),
        ]

    def get_displayed_entries(self):
        """Return a dictionary of actions that should be shown for a game"""
        return {
            "add":
            not self.game.is_installed,
            "install":
            not self.game.is_installed,
            "play":
            self.game.is_installed and not self.is_game_running,
            "stop":
            self.is_game_running,
            "configure":
            bool(self.game.is_installed),
            "browse":
            self.game.is_installed and self.game.runner_name != "browser",
            "show_logs":
            self.game.is_installed,
            "favorite":
            not self.game.is_favorite,
            "deletefavorite":
            self.game.is_favorite,
            "install_more":
            not self.game.service and self.game.is_installed,
            "execute-script":
            bool(self.game.is_installed
                 and self.game.runner.system_config.get("manual_command")),
            "desktop-shortcut": (self.game.is_installed
                                 and not xdgshortcuts.desktop_launcher_exists(
                                     self.game.slug, self.game.id)),
            "menu-shortcut": (self.game.is_installed
                              and not xdgshortcuts.menu_launcher_exists(
                                  self.game.slug, self.game.id)),
            "rm-desktop-shortcut":
            bool(self.game.is_installed
                 and xdgshortcuts.desktop_launcher_exists(
                     self.game.slug, self.game.id)),
            "rm-menu-shortcut":
            bool(self.game.is_installed and xdgshortcuts.menu_launcher_exists(
                self.game.slug, self.game.id)),
            "remove":
            True,
            "view":
            True,
            "hide":
            self.game.is_installed and not self.game.is_hidden,
            "unhide":
            self.game.is_hidden,
        }

    def on_game_launch(self, *_args):
        """Launch a game"""
        self.game.launch()

    def get_running_game(self):
        ids = self.application.get_running_game_ids()
        for game_id in ids:
            if str(game_id) == str(self.game.id):
                return self.game
        logger.warning("Game %s not in %s", self.game_id, ids)

    def on_game_stop(self, caller):  # pylint: disable=unused-argument
        """Stops the game"""
        matched_game = self.get_running_game()
        if not matched_game:
            return
        if not matched_game.game_thread:
            logger.warning(
                "Game %s doesn't appear to be running, not killing it",
                self.game_id)
            return
        try:
            os.kill(matched_game.game_thread.game_process.pid, signal.SIGTERM)
        except ProcessLookupError as ex:
            logger.debug("Failed to kill game process: %s", ex)

    def on_show_logs(self, _widget):
        """Display game log"""
        _buffer = LOG_BUFFERS.get(self.game.id)
        if not _buffer:
            logger.info("No log for game %s", self.game)
        return LogWindow(title=_("Log for {}").format(self.game),
                         buffer=_buffer,
                         application=self.application)

    def on_install_clicked(self, *_args):
        """Install a game"""
        # Install the currently selected game in the UI
        self.game.emit("game-install")

    def on_add_manually(self, _widget, *_args):
        """Callback that presents the Add game dialog"""
        AddGameDialog(self.window,
                      game=self.game,
                      runner=self.game.runner_name)

    def on_edit_game_configuration(self, _widget):
        """Edit game preferences"""
        EditGameConfigDialog(self.window, self.game)

    def on_add_favorite_game(self, _widget):
        """Add to favorite Games list"""
        self.game.add_to_favorites()

    def on_delete_favorite_game(self, _widget):
        """delete from favorites"""
        self.game.remove_from_favorites()

    def on_hide_game(self, _widget):
        """Add a game to the list of hidden games"""
        self.game.hide()

    def on_unhide_game(self, _widget):
        """Removes a game from the list of hidden games"""
        self.game.unhide()

    def on_execute_script_clicked(self, _widget):
        """Execute the game's associated script"""
        manual_command = self.game.runner.system_config.get("manual_command")
        if path_exists(manual_command):
            MonitoredCommand(
                [manual_command],
                include_processes=[os.path.basename(manual_command)],
                cwd=self.game.directory,
            ).start()
            logger.info("Running %s in the background", manual_command)

    def on_browse_files(self, _widget):
        """Callback to open a game folder in the file browser"""
        path = self.game.get_browse_dir()
        if not path:
            dialogs.NoticeDialog(_("This game has no installation directory"))
        elif path_exists(path):
            open_uri("file://%s" % path)
        else:
            dialogs.NoticeDialog(
                _("Can't open %s \nThe folder doesn't exist.") % path)

    def on_create_menu_shortcut(self, *_args):
        """Add the selected game to the system's Games menu."""
        xdgshortcuts.create_launcher(self.game.slug,
                                     self.game.id,
                                     self.game.name,
                                     menu=True)

    def on_create_desktop_shortcut(self, *_args):
        """Create a desktop launcher for the selected game."""
        xdgshortcuts.create_launcher(self.game.slug,
                                     self.game.id,
                                     self.game.name,
                                     desktop=True)

    def on_remove_menu_shortcut(self, *_args):
        """Remove an XDG menu shortcut"""
        xdgshortcuts.remove_launcher(self.game.slug, self.game.id, menu=True)

    def on_remove_desktop_shortcut(self, *_args):
        """Remove a .desktop shortcut"""
        xdgshortcuts.remove_launcher(self.game.slug,
                                     self.game.id,
                                     desktop=True)

    def on_view_game(self, _widget):
        """Callback to open a game on lutris.net"""
        open_uri("https://lutris.net/games/%s" % self.game.slug)

    def on_remove_game(self, *_args):
        """Callback that present the uninstall dialog to the user"""
        if self.game.is_installed:
            UninstallGameDialog(game_id=self.game.id, parent=self.window)
        else:
            RemoveGameDialog(game_id=self.game.id, parent=self.window)
Ejemplo n.º 30
0
class GameDialogCommon:
    """Mixin for config dialogs"""
    no_runner_label = "Select a runner in the Game Info tab"

    def __init__(self):
        self.notebook = None
        self.vbox = None
        self.name_entry = None
        self.runner_box = None

        self.timer_id = None
        self.game = None
        self.saved = None
        self.slug = None
        self.slug_entry = None
        self.year_entry = None
        self.slug_change_button = None
        self.runner_dropdown = None
        self.banner_button = None
        self.icon_button = None
        self.game_box = None
        self.system_box = None
        self.system_sw = None
        self.runner_name = None
        self.runner_index = None
        self.lutris_config = None
        self.clipboard = None
        self._clipboard_buffer = None

    @staticmethod
    def build_scrolled_window(widget):
        """Return a scrolled window for containing config widgets"""
        scrolled_window = Gtk.ScrolledWindow()
        scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC,
                                   Gtk.PolicyType.AUTOMATIC)
        scrolled_window.add(widget)
        return scrolled_window

    def build_notebook(self):
        self.notebook = Gtk.Notebook()
        self.vbox.pack_start(self.notebook, True, True, 10)

    def build_tabs(self, config_level):
        self.timer_id = None
        if config_level == "game":
            self._build_info_tab()
            self._build_game_tab()
        if config_level in ("game", "runner"):
            self._build_runner_tab(config_level)
        if config_level == "system":
            self._build_prefs_tab()
            self._build_sysinfo_tab()
        self._build_system_tab(config_level)

    def _build_info_tab(self):
        info_box = VBox()

        if self.game:
            info_box.pack_start(self._get_banner_box(), False, False,
                                6)  # Banner

        info_box.pack_start(self._get_name_box(), False, False, 6)  # Game name

        if self.game:
            info_box.pack_start(self._get_slug_box(), False, False,
                                6)  # Game id

        self.runner_box = self._get_runner_box()
        info_box.pack_start(self.runner_box, False, False, 6)  # Runner

        info_box.pack_start(self._get_year_box(), False, False, 6)  # Year

        info_sw = self.build_scrolled_window(info_box)
        self._add_notebook_tab(info_sw, "Game info")

    def _build_prefs_tab(self):
        prefs_box = VBox()
        prefs_box.pack_start(self._get_game_cache_box(), False, False, 6)

        cache_help_label = Gtk.Label(visible=True)
        cache_help_label.set_size_request(400, -1)
        cache_help_label.set_markup(
            "If provided, this location will be used by installers to cache "
            "downloaded files locally for future re-use. \nIf left empty, the "
            "installer files are discarded after the install completion.")
        prefs_box.pack_start(cache_help_label, False, False, 6)

        info_sw = self.build_scrolled_window(prefs_box)
        self._add_notebook_tab(info_sw, "Lutris preferences")

    def _build_sysinfo_tab(self):
        sysinfo_grid = Gtk.Grid()
        sysinfo_view = Gtk.TextView()
        sysinfo_view.set_editable(False)
        sysinfo_view.set_cursor_visible(False)
        sysinfo_str = gather_system_info_str()

        text_buffer = sysinfo_view.get_buffer()
        text_buffer.set_text(sysinfo_str)
        self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        self._clipboard_buffer = sysinfo_str

        button_copy = Gtk.Button("Copy System Info")
        button_copy.connect("clicked", self._copy_text)

        sysinfo_grid.add(sysinfo_view)
        sysinfo_grid.attach_next_to(button_copy, sysinfo_view,
                                    Gtk.PositionType.BOTTOM, 1, 1)
        info_sw = self.build_scrolled_window(sysinfo_grid)
        self._add_notebook_tab(info_sw, "System Information")

    def _copy_text(self, widget):
        self.clipboard.set_text(self._clipboard_buffer, -1)

    def _get_game_cache_box(self):
        box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)
        label = Label("Cache path")
        box.pack_start(label, False, False, 0)
        cache_path = get_cache_path()
        path_chooser = FileChooserEntry(
            title="Set the folder for the cache path",
            action=Gtk.FileChooserAction.SELECT_FOLDER,
            path=cache_path)
        path_chooser.entry.connect("changed", self._on_cache_path_set)
        box.pack_start(path_chooser, True, True, 0)
        return box

    def _on_cache_path_set(self, entry):
        if self.timer_id:
            GLib.source_remove(self.timer_id)
        self.timer_id = GLib.timeout_add(1000, self.save_cache_setting,
                                         entry.get_text())

    def save_cache_setting(self, value):
        save_cache_path(value)
        GLib.source_remove(self.timer_id)
        self.timer_id = None
        return False

    def _get_name_box(self):
        box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)
        label = Label("Name")
        box.pack_start(label, False, False, 0)
        self.name_entry = Gtk.Entry()
        if self.game:
            self.name_entry.set_text(self.game.name)
        box.pack_start(self.name_entry, True, True, 0)
        return box

    def _get_slug_box(self):
        slug_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)

        label = Label("Identifier")
        slug_box.pack_start(label, False, False, 0)

        self.slug_entry = SlugEntry()
        self.slug_entry.set_text(self.game.slug)
        self.slug_entry.set_sensitive(False)
        self.slug_entry.connect("activate", self.on_slug_entry_activate)
        slug_box.pack_start(self.slug_entry, True, True, 0)

        self.slug_change_button = Gtk.Button("Change")
        self.slug_change_button.connect("clicked", self.on_slug_change_clicked)
        slug_box.pack_start(self.slug_change_button, False, False, 0)

        return slug_box

    def _get_runner_box(self):
        runner_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)

        runner_label = Label("Runner")
        runner_box.pack_start(runner_label, False, False, 0)

        self.runner_dropdown = self._get_runner_dropdown()
        runner_box.pack_start(self.runner_dropdown, True, True, 0)

        install_runners_btn = Gtk.Button("Install runners")
        install_runners_btn.connect("clicked", self.on_install_runners_clicked)
        runner_box.pack_start(install_runners_btn, True, True, 0)

        return runner_box

    def _get_banner_box(self):
        banner_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)

        label = Label("")
        banner_box.pack_start(label, False, False, 0)

        self.banner_button = Gtk.Button()
        self._set_image("banner")
        self.banner_button.connect("clicked", self.on_custom_image_select,
                                   "banner")
        banner_box.pack_start(self.banner_button, False, False, 0)

        reset_banner_button = Gtk.Button.new_from_icon_name(
            "edit-clear", Gtk.IconSize.MENU)
        reset_banner_button.set_relief(Gtk.ReliefStyle.NONE)
        reset_banner_button.set_tooltip_text("Remove custom banner")
        reset_banner_button.connect("clicked",
                                    self.on_custom_image_reset_clicked,
                                    "banner")
        banner_box.pack_start(reset_banner_button, False, False, 0)

        self.icon_button = Gtk.Button()
        self._set_image("icon")
        self.icon_button.connect("clicked", self.on_custom_image_select,
                                 "icon")
        banner_box.pack_start(self.icon_button, False, False, 0)

        reset_icon_button = Gtk.Button.new_from_icon_name(
            "edit-clear", Gtk.IconSize.MENU)
        reset_icon_button.set_relief(Gtk.ReliefStyle.NONE)
        reset_icon_button.set_tooltip_text("Remove custom icon")
        reset_icon_button.connect("clicked",
                                  self.on_custom_image_reset_clicked, "icon")
        banner_box.pack_start(reset_icon_button, False, False, 0)

        return banner_box

    def _get_year_box(self):
        box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)

        label = Label("Release year")
        box.pack_start(label, False, False, 0)

        self.year_entry = NumberEntry()
        if self.game:
            self.year_entry.set_text(str(self.game.year or ""))
        box.pack_start(self.year_entry, True, True, 0)

        return box

    def _set_image(self, image_format):
        image = Gtk.Image()
        game_slug = self.game.slug if self.game else ""
        image.set_from_pixbuf(get_pixbuf_for_game(game_slug, image_format))
        if image_format == "banner":
            self.banner_button.set_image(image)
        else:
            self.icon_button.set_image(image)

    def _set_icon_image(self):
        image = Gtk.Image()
        game_slug = self.game.slug if self.game else ""
        image.set_from_pixbuf(get_pixbuf_for_game(game_slug, "banner"))
        self.banner_button.set_image(image)

    def _get_runner_dropdown(self):
        runner_liststore = self._get_runner_liststore()
        runner_dropdown = Gtk.ComboBox.new_with_model(runner_liststore)
        runner_dropdown.set_id_column(1)
        runner_index = 0
        if self.runner_name:
            for runner in runner_liststore:
                if self.runner_name == str(runner[1]):
                    break
                runner_index += 1
        self.runner_index = runner_index
        runner_dropdown.set_active(self.runner_index)
        runner_dropdown.connect("changed", self.on_runner_changed)
        cell = Gtk.CellRendererText()
        cell.props.ellipsize = Pango.EllipsizeMode.END
        runner_dropdown.pack_start(cell, True)
        runner_dropdown.add_attribute(cell, "text", 0)
        return runner_dropdown

    @staticmethod
    def _get_runner_liststore():
        """Build a ListStore with available runners."""
        runner_liststore = Gtk.ListStore(str, str)
        runner_liststore.append(("Select a runner from the list", ""))
        for runner in runners.get_installed():
            description = runner.description
            runner_liststore.append(
                ("%s (%s)" % (runner.human_name, description), runner.name))
        return runner_liststore

    def on_slug_change_clicked(self, widget):
        if self.slug_entry.get_sensitive() is False:
            widget.set_label("Apply")
            self.slug_entry.set_sensitive(True)
        else:
            self.change_game_slug()

    def on_slug_entry_activate(self, _widget):
        self.change_game_slug()

    def change_game_slug(self):
        self.slug = self.slug_entry.get_text()
        self.slug_entry.set_sensitive(False)
        self.slug_change_button.set_label("Change")

    def on_install_runners_clicked(self, _button):
        """Messed up callback requiring an import in the method to avoid a circular dependency"""
        from lutris.gui.dialogs.runners import RunnersDialog
        runners_dialog = RunnersDialog()
        runners_dialog.connect("runner-installed",
                               self._update_runner_dropdown)

    def _update_runner_dropdown(self, _widget):
        active_id = self.runner_dropdown.get_active_id()
        self.runner_dropdown.set_model(self._get_runner_liststore())
        self.runner_dropdown.set_active_id(active_id)

    def _build_game_tab(self):
        if self.game and self.runner_name:
            self.game.runner_name = self.runner_name
            if not self.game.runner or self.game.runner.name != self.runner_name:
                try:
                    self.game.runner = runners.import_runner(
                        self.runner_name)()
                except runners.InvalidRunner:
                    pass
            self.game_box = GameBox(self.lutris_config, self.game)
            game_sw = self.build_scrolled_window(self.game_box)
        elif self.runner_name:
            game = Game(None)
            game.runner_name = self.runner_name
            self.game_box = GameBox(self.lutris_config, game)
            game_sw = self.build_scrolled_window(self.game_box)
        else:
            game_sw = Gtk.Label(label=self.no_runner_label)
        self._add_notebook_tab(game_sw, "Game options")

    def _build_runner_tab(self, _config_level):
        if self.runner_name:
            self.runner_box = RunnerBox(self.lutris_config, self.game)
            runner_sw = self.build_scrolled_window(self.runner_box)
        else:
            runner_sw = Gtk.Label(label=self.no_runner_label)
        self._add_notebook_tab(runner_sw, "Runner options")

    def _build_system_tab(self, _config_level):
        if not self.lutris_config:
            raise RuntimeError("Lutris config not loaded yet")
        self.system_box = SystemBox(self.lutris_config)
        self.system_sw = self.build_scrolled_window(self.system_box)
        self._add_notebook_tab(self.system_sw, "System options")

    def _add_notebook_tab(self, widget, label):
        self.notebook.append_page(widget, Gtk.Label(label=label))

    def build_action_area(self, button_callback):
        self.action_area.set_layout(Gtk.ButtonBoxStyle.EDGE)

        # Advanced settings checkbox
        checkbox = Gtk.CheckButton(label="Show advanced options")
        value = settings.read_setting("show_advanced_options")
        if value == "True":
            checkbox.set_active(value)
        checkbox.connect("toggled", self.on_show_advanced_options_toggled)
        self.action_area.pack_start(checkbox, False, False, 5)

        # Buttons
        hbox = Gtk.Box()
        cancel_button = Gtk.Button(label="Cancel")
        cancel_button.connect("clicked", self.on_cancel_clicked)
        hbox.pack_start(cancel_button, True, True, 10)

        save_button = Gtk.Button(label="Save")
        save_button.connect("clicked", button_callback)
        hbox.pack_start(save_button, True, True, 0)
        self.action_area.pack_start(hbox, True, True, 0)

    def on_show_advanced_options_toggled(self, checkbox):
        value = True if checkbox.get_active() else False
        settings.write_setting("show_advanced_options", value)

        self._set_advanced_options_visible(value)

    def _set_advanced_options_visible(self, value):
        """Change visibility of advanced options across all config tabs."""
        widgets = self.system_box.get_children()
        if self.runner_name:
            widgets += self.runner_box.get_children()
        if self.game:
            widgets += self.game_box.get_children()

        for widget in widgets:
            if widget.get_style_context().has_class("advanced"):
                widget.set_visible(value)
                if value:
                    widget.set_no_show_all(not value)
                    widget.show_all()

    def on_runner_changed(self, widget):
        """Action called when runner drop down is changed."""
        new_runner_index = widget.get_active()
        if self.runner_index and new_runner_index != self.runner_index:
            dlg = QuestionDialog({
                "question":
                "Are you sure you want to change the runner for this game ? "
                "This will reset the full configuration for this game and "
                "is not reversible.",
                "title":
                "Confirm runner change",
            })

            if dlg.result == Gtk.ResponseType.YES:
                self.runner_index = new_runner_index
                self._switch_runner(widget)
            else:
                # Revert the dropdown menu to the previously selected runner
                widget.set_active(self.runner_index)
        else:
            self.runner_index = new_runner_index
            self._switch_runner(widget)

    def _switch_runner(self, widget):
        """Rebuilds the UI on runner change"""
        current_page = self.notebook.get_current_page()
        if self.runner_index == 0:
            self.runner_name = None
            self.lutris_config = None
        else:
            self.runner_name = widget.get_model()[self.runner_index][1]
            self.lutris_config = LutrisConfig(runner_slug=self.runner_name,
                                              level="game")
        self._rebuild_tabs()
        self.notebook.set_current_page(current_page)

    def _rebuild_tabs(self):
        for i in range(self.notebook.get_n_pages(), 1, -1):
            self.notebook.remove_page(i - 1)
        self._build_game_tab()
        self._build_runner_tab("game")
        self._build_system_tab("game")
        self.show_all()

    def on_cancel_clicked(self, _widget=None, _event=None):
        """Dialog destroy callback."""
        if self.game:
            self.game.load_config()
        self.destroy()

    def is_valid(self):
        if not self.runner_name:
            ErrorDialog("Runner not provided")
            return False
        if not self.name_entry.get_text():
            ErrorDialog("Please fill in the name")
            return False
        if (self.runner_name in ("steam", "winesteam")
                and self.lutris_config.game_config.get("appid") is None):
            ErrorDialog("Steam AppId not provided")
            return False
        return True

    def on_save(self, _button):
        """Save game info and destroy widget. Return True if success."""
        if not self.is_valid():
            logger.warning(
                "Current configuration is not valid, ignoring save request")
            return False
        name = self.name_entry.get_text()

        if not self.slug:
            self.slug = slugify(name)

        if not self.game:
            self.game = Game()

        year = None
        if self.year_entry.get_text():
            year = int(self.year_entry.get_text())

        if not self.lutris_config.game_config_id:
            self.lutris_config.game_config_id = make_game_config_id(self.slug)

        runner_class = runners.import_runner(self.runner_name)
        runner = runner_class(self.lutris_config)

        self.game.name = name
        self.game.slug = self.slug
        self.game.year = year
        self.game.game_config_id = self.lutris_config.game_config_id
        self.game.runner = runner
        self.game.runner_name = self.runner_name
        self.game.directory = runner.game_path
        self.game.is_installed = True
        if self.runner_name in ("steam", "winesteam"):
            self.game.steamid = self.lutris_config.game_config["appid"]

        self.game.config = self.lutris_config
        self.game.save()
        self.destroy()
        self.saved = True

    def on_custom_image_select(self, _widget, image_type):
        dialog = Gtk.FileChooserDialog(
            "Please choose a custom image",
            self,
            Gtk.FileChooserAction.OPEN,
            (
                Gtk.STOCK_CANCEL,
                Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OPEN,
                Gtk.ResponseType.OK,
            ),
        )

        image_filter = Gtk.FileFilter()
        image_filter.set_name("Images")
        image_filter.add_pixbuf_formats()
        dialog.add_filter(image_filter)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            image_path = dialog.get_filename()
            if image_type == "banner":
                self.game.has_custom_banner = True
                dest_path = resources.get_banner_path(self.game.slug)
                size = BANNER_SIZE
                file_format = "jpeg"
            else:
                self.game.has_custom_icon = True
                dest_path = resources.get_icon_path(self.game.slug)
                size = ICON_SIZE
                file_format = "png"
            pixbuf = get_pixbuf(image_path, size)
            pixbuf.savev(dest_path, file_format, [], [])
            self._set_image(image_type)

            if image_type == "icon":
                resources.update_desktop_icons()

        dialog.destroy()

    def on_custom_image_reset_clicked(self, _widget, image_type):
        if image_type == "banner":
            self.game.has_custom_banner = False
            dest_path = resources.get_banner_path(self.game.slug)
        elif image_type == "icon":
            self.game.has_custom_icon = False
            dest_path = resources.get_icon_path(self.game.slug)
        else:
            raise ValueError("Unsupported image type %s" % image_type)
        os.remove(dest_path)
        self._set_image(image_type)
Ejemplo n.º 31
0
 def create_desktop_shortcut(self, *args):
     """Create a desktop launcher for the selected game."""
     game = Game(self.view.selected_game)
     xdg.create_launcher(game.slug, game.id, game.name, desktop=True)
Ejemplo n.º 32
0
class UninstallGameDialog(GtkBuilderDialog):
    glade_file = 'dialog-uninstall-game.ui'
    dialog_object = 'uninstall-game-dialog'

    def substitute_label(self, widget, name, replacement):
        if hasattr(widget, 'get_text'):
            get_text = widget.get_text
            set_text = widget.set_text
        elif hasattr(widget, 'get_label'):
            get_text = widget.get_label
            set_text = widget.set_label
        else:
            raise TypeError("Unsupported type %s" % type(widget))

        replacement = replacement.replace('&', '&amp;')
        set_text(get_text().replace("{%s}" % name, replacement))

    def initialize(self, slug=None, callback=None):
        self.game = Game(slug)
        self.callback = callback
        runner = self.game.runner

        self.substitute_label(self.builder.get_object('description_label'),
                              'game', self.game.name)

        self.substitute_label(
            self.builder.get_object('remove_from_library_button'),
            'game', self.game.name
        )
        remove_contents_button = self.builder.get_object(
            'remove_contents_button'
        )
        if self.game.is_installed:
            if hasattr(runner, 'own_game_remove_method'):
                remove_contents_button.set_label(runner.own_game_remove_method)
            else:
                try:
                    default_path = runner.default_path
                except AttributeError:
                    default_path = "/"
                try:
                    game_path = runner.game_path
                except AttributeError:
                    game_path = '/'
                if not is_removeable(game_path, excludes=[default_path]):
                    remove_contents_button.set_sensitive(False)

            path = self.game.directory or 'disk'
            self.substitute_label(remove_contents_button, 'path', path)
            remove_contents_button.get_children()[0].set_use_markup(True)
        else:
            remove_contents_button.hide()

        cancel_button = self.builder.get_object('cancel_button')
        cancel_button.connect('clicked', self.on_close)

        apply_button = self.builder.get_object('apply_button')
        apply_button.connect('clicked', self.on_apply_button_clicked)

    def on_apply_button_clicked(self, widget):
        widget.set_sensitive(False)

        remove_from_library_button = self.builder.get_object(
            'remove_from_library_button'
        )
        remove_from_library = remove_from_library_button.get_active()
        remove_contents_button = self.builder.get_object(
            'remove_contents_button'
        )
        remove_contents = remove_contents_button.get_active()
        if remove_contents and not hasattr(self.game.runner,
                                           'no_game_remove_warning'):
            game_dir = self.game.directory.replace('&', '&amp;')
            dlg = QuestionDialog({
                'question': "Are you sure you want to delete EVERYTHING under "
                            "\n<b>%s</b>?\n (This can't be undone)"
                            % game_dir,
                'title': "CONFIRM DANGEROUS OPERATION"
            })
            if dlg.result != Gtk.ResponseType.YES:
                widget.set_sensitive(True)
                return

        self.game.remove(remove_from_library, remove_contents)
        self.callback(self.game.slug, remove_from_library)

        self.on_close()
Ejemplo n.º 33
0
    def popup(self, event, game_row=None, game=None):
        if game_row:
            game_id = game_row[COL_ID]
            game_slug = game_row[COL_SLUG]
            runner_slug = game_row[COL_RUNNER]
            is_installed = game_row[COL_INSTALLED]
        elif game:
            game_id = game.id
            game_slug = game.slug
            runner_slug = game.runner_name
            is_installed = game.is_installed

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

        # Main items
        self.add_menuitems(self.main_entries)
        # Runner specific items
        runner_entries = None
        if runner_slug:
            game = game or Game(game_id)
            try:
                runner = import_runner(runner_slug)(game.config)
            except InvalidRunner:
                runner_entries = None
            else:
                runner_entries = runner.context_menu_entries
        if runner_entries:
            self.append(Gtk.SeparatorMenuItem())
            self.add_menuitems(runner_entries)
        self.show_all()

        # Hide some items
        hiding_condition = {
            'add':
            is_installed,
            'install':
            is_installed,
            'install_more':
            not is_installed,
            'play':
            not is_installed,
            'configure':
            not is_installed,
            'desktop-shortcut':
            (not is_installed
             or xdg.desktop_launcher_exists(game_slug, game_id)),
            'menu-shortcut': (not is_installed
                              or xdg.menu_launcher_exists(game_slug, game_id)),
            'rm-desktop-shortcut':
            (not is_installed
             or not xdg.desktop_launcher_exists(game_slug, game_id)),
            'rm-menu-shortcut':
            (not is_installed
             or not xdg.menu_launcher_exists(game_slug, game_id)),
            'browse':
            not is_installed or runner_slug == 'browser',
        }
        for menuitem in self.get_children():
            if type(menuitem) is not Gtk.ImageMenuItem:
                continue
            action = menuitem.action_id
            visible = not hiding_condition.get(action)
            menuitem.set_visible(visible)

        super(ContextualMenu, self).popup(None, None, None, None, event.button,
                                          event.time)
Ejemplo n.º 34
0
    def _write_config(self):
        """Write the game configuration in the DB and config file."""
        if self.extends:
            logger.info(
                'This is an extension to %s, not creating a new game entry',
                self.extends)
            return
        configpath = make_game_config_id(self.slug)
        config_filename = os.path.join(settings.CONFIG_DIR,
                                       "games/%s.yml" % configpath)

        if self.requires:
            # Load the base game config
            required_game = pga.get_game_by_field(self.requires,
                                                  field='installer_slug')
            base_config = LutrisConfig(
                runner_slug=self.runner,
                game_config_id=required_game['configpath'])
            config = base_config.game_level
        else:
            config = {
                'game': {},
            }

        self.game_id = pga.add_or_update(name=self.game_name,
                                         runner=self.runner,
                                         slug=self.game_slug,
                                         directory=self.target_path,
                                         installed=1,
                                         installer_slug=self.slug,
                                         parent_slug=self.requires,
                                         year=self.year,
                                         steamid=self.steamid,
                                         configpath=configpath,
                                         id=self.game_id)

        game = Game(self.game_id)
        game.set_platform_from_runner()
        game.save()

        logger.debug("Saved game entry %s (%d)", self.game_slug, self.game_id)

        # Config update
        if 'system' in self.script:
            config['system'] = self._substitute_config(self.script['system'])
        if self.runner in self.script and self.script[self.runner]:
            config[self.runner] = self._substitute_config(
                self.script[self.runner])

        # Game options such as exe or main_file can be added at the root of the
        # script as a shortcut, this integrates them into the game config
        # properly
        launcher, launcher_value = self._get_game_launcher()
        if type(launcher_value) == list:
            game_files = []
            for game_file in launcher_value:
                if game_file in self.game_files:
                    game_files.append(self.game_files[game_file])
                else:
                    game_files.append(game_file)
            config['game'][launcher] = game_files
        elif launcher_value:
            if launcher_value in self.game_files:
                launcher_value = (self.game_files[launcher_value])
            elif self.target_path and os.path.exists(
                    os.path.join(self.target_path, launcher_value)):
                launcher_value = os.path.join(self.target_path, launcher_value)
            config['game'][launcher] = launcher_value

        if 'game' in self.script:
            config['game'].update(self.script['game'])
            config['game'] = self._substitute_config(config['game'])

            # steamless_binary64 can be used to specify 64 bit non-steam binaries
            if system.IS_64BIT and 'steamless_binary64' in config['game']:
                config['game']['steamless_binary'] = config['game'][
                    'steamless_binary64']

        yaml_config = yaml.safe_dump(config, default_flow_style=False)
        with open(config_filename, "w") as config_file:
            config_file.write(yaml_config)
Ejemplo n.º 35
0
 def on_view_game(self, widget):
     game = Game(self.view.selected_game)
     self._open_browser('https://lutris.net/games/' + game.slug)
Ejemplo n.º 36
0
class UninstallGameDialog(Dialog):
    def __init__(self, game_id, parent=None):
        super().__init__(parent=parent)
        self.set_size_request(640, 128)
        self.game = Game(game_id)
        self.delete_files = False
        container = Gtk.VBox(visible=True)
        self.get_content_area().add(container)

        title_label = Gtk.Label(visible=True)
        title_label.set_line_wrap(True)
        title_label.set_alignment(0, 0.5)
        title_label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
        title_label.set_markup(
            "<span font_desc='14'><b>Uninstall %s</b></span>" %
            gtk_safe(self.game.name))

        container.pack_start(title_label, False, False, 4)

        self.folder_label = Gtk.Label(visible=True)
        self.folder_label.set_alignment(0, 0.5)

        self.delete_button = Gtk.Button(_("Uninstall"), visible=True)
        self.delete_button.connect("clicked", self.on_delete_clicked)

        if not self.game.directory:
            self.folder_label.set_markup("No file will be deleted")
        elif len(get_games(searches={"directory": self.game.directory})) > 1:
            self.folder_label.set_markup(
                "The folder %s is used by other games and will be kept." %
                self.game.directory)
        elif is_removeable(self.game.directory):
            self.delete_button.set_sensitive(False)
            self.folder_label.set_markup("<i>Calculating size…</i>")
            AsyncCall(get_disk_size, self.folder_size_cb, self.game.directory)
        else:
            self.folder_label.set_markup(
                "Content of %s are protected and will not be deleted." %
                reverse_expanduser(self.game.directory))
        container.pack_start(self.folder_label, False, False, 4)

        self.confirm_delete_button = Gtk.CheckButton()
        self.confirm_delete_button.set_active(True)
        container.pack_start(self.confirm_delete_button, False, False, 4)

        button_box = Gtk.HBox(visible=True)
        button_box.set_margin_top(30)
        style_context = button_box.get_style_context()
        style_context.add_class("linked")
        cancel_button = Gtk.Button(_("Cancel"), visible=True)
        cancel_button.connect("clicked", self.on_close)
        button_box.add(cancel_button)
        button_box.add(self.delete_button)
        container.pack_end(button_box, False, False, 0)
        self.show()

    def folder_size_cb(self, folder_size, error):
        if error:
            logger.error(error)
            return
        self.delete_files = True
        self.delete_button.set_sensitive(True)
        self.folder_label.hide()
        self.confirm_delete_button.show()
        self.confirm_delete_button.set_label(
            "Delete %s (%s)" %
            (reverse_expanduser(self.game.directory), human_size(folder_size)))

    def on_close(self, _button):
        self.destroy()

    def on_delete_clicked(self, button):
        button.set_sensitive(False)
        if not self.confirm_delete_button.get_active():
            self.delete_files = False
        if self.delete_files and not hasattr(self.game.runner,
                                             "no_game_remove_warning"):
            dlg = QuestionDialog({
                "question":
                _("Please confirm.\nEverything under <b>%s</b>\n"
                  "will be deleted.") % gtk_safe(self.game.directory),
                "title":
                _("Permanently delete files?"),
            })
            if dlg.result != Gtk.ResponseType.YES:
                button.set_sensitive(True)
                return
        if self.delete_files:
            self.folder_label.set_markup(
                "Uninstalling game and deleting files...")
        else:
            self.folder_label.set_markup("Uninstalling game...")
        self.game.remove(self.delete_files)
        self.destroy()
Ejemplo n.º 37
0
class GameDialogCommon(object):
    no_runner_label = "Select a runner in the Game Info tab"

    @staticmethod
    def build_scrolled_window(widget):
        scrolled_window = Gtk.ScrolledWindow()
        scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC,
                                   Gtk.PolicyType.AUTOMATIC)
        scrolled_window.add_with_viewport(widget)
        return scrolled_window

    def build_notebook(self):
        self.notebook = Gtk.Notebook()
        self.vbox.pack_start(self.notebook, True, True, 10)

    def build_tabs(self, config_level):
        if config_level == 'game':
            self._build_info_tab()
            self._build_game_tab()
        self._build_runner_tab(config_level)
        self._build_system_tab(config_level)

    def _build_info_tab(self):
        info_box = VBox()

        info_box.pack_start(self._get_name_box(), False, False, 5)  # Game name

        if self.game:
            info_box.pack_start(self._get_slug_box(), False, False,
                                5)  # Game id
            info_box.pack_start(self._get_banner_box(), False, False,
                                5)  # Banner

        self.runner_box = self._get_runner_box()
        info_box.pack_start(self.runner_box, False, False, 5)  # Runner

        info_sw = self.build_scrolled_window(info_box)
        self._add_notebook_tab(info_sw, "Game info")

    def _get_name_box(self):
        box = Gtk.HBox()

        label = Gtk.Label(label="Name")
        box.pack_start(label, False, False, 20)

        self.name_entry = Gtk.Entry()
        if self.game:
            self.name_entry.set_text(self.game.name)
        box.pack_start(self.name_entry, True, True, 20)

        return box

    def _get_slug_box(self):
        box = Gtk.HBox()

        label = Gtk.Label(label="Identifier")
        box.pack_start(label, False, False, 20)

        self.slug_entry = SlugEntry()
        self.slug_entry.set_text(self.game.slug)
        self.slug_entry.set_sensitive(False)
        self.slug_entry.connect('activate', self.on_slug_entry_activate)
        box.pack_start(self.slug_entry, True, True, 0)

        slug_change_button = Gtk.Button("Change")
        slug_change_button.connect('clicked', self.on_slug_change_clicked)
        box.pack_start(slug_change_button, False, False, 20)

        return box

    def _get_runner_box(self):
        runner_box = Gtk.HBox()
        runner_label = Gtk.Label("Runner")
        runner_label.set_alignment(0.5, 0.5)
        self.runner_dropdown = self._get_runner_dropdown()
        install_runners_btn = Gtk.Button(label="Install runners")
        install_runners_btn.connect('clicked', self.on_install_runners_clicked)
        install_runners_btn.set_margin_right(20)

        runner_box.pack_start(runner_label, False, False, 20)
        runner_box.pack_start(self.runner_dropdown, False, False, 20)
        runner_box.pack_start(install_runners_btn, False, False, 0)
        return runner_box

    def _get_banner_box(self):
        banner_box = Gtk.HBox()
        banner_label = Gtk.Label("Banner")
        banner_label.set_alignment(0.5, 0.5)
        self.banner_button = Gtk.Button()
        self._set_image('banner')
        self.banner_button.connect('clicked', self.on_custom_image_select,
                                   'banner')

        reset_banner_button = Gtk.Button.new_from_icon_name(
            'edit-clear', Gtk.IconSize.MENU)
        reset_banner_button.set_relief(Gtk.ReliefStyle.NONE)
        reset_banner_button.set_tooltip_text("Remove custom banner")
        reset_banner_button.connect('clicked',
                                    self.on_custom_image_reset_clicked,
                                    'banner')

        self.icon_button = Gtk.Button()
        self._set_image('icon')
        self.icon_button.connect('clicked', self.on_custom_image_select,
                                 'icon')

        reset_icon_button = Gtk.Button.new_from_icon_name(
            'edit-clear', Gtk.IconSize.MENU)
        reset_icon_button.set_relief(Gtk.ReliefStyle.NONE)
        reset_icon_button.set_tooltip_text("Remove custom icon")
        reset_icon_button.connect('clicked',
                                  self.on_custom_image_reset_clicked, 'icon')

        banner_box.pack_start(banner_label, False, False, 20)
        banner_box.pack_start(self.banner_button, False, False, 0)
        banner_box.pack_start(reset_banner_button, False, False, 0)
        banner_box.pack_start(self.icon_button, False, False, 0)
        banner_box.pack_start(reset_icon_button, False, False, 0)
        return banner_box

    def _set_image(self, image_format):
        assert image_format in ('banner', 'icon')
        image = Gtk.Image()
        game_slug = self.game.slug if self.game else ''
        image.set_from_pixbuf(get_pixbuf_for_game(game_slug, image_format))
        if image_format == 'banner':
            self.banner_button.set_image(image)
        else:
            self.icon_button.set_image(image)

    def _set_icon_image(self):
        image = Gtk.Image()
        game_slug = self.game.slug if self.game else ''
        image.set_from_pixbuf(get_pixbuf_for_game(game_slug, 'banner'))
        self.banner_button.set_image(image)

    def _get_runner_dropdown(self):
        runner_liststore = self._get_runner_liststore()
        runner_dropdown = Gtk.ComboBox.new_with_model(runner_liststore)
        runner_dropdown.set_id_column(1)
        runner_index = 0
        if self.runner_name:
            for runner in runner_liststore:
                if self.runner_name == str(runner[1]):
                    break
                runner_index += 1
        runner_dropdown.set_active(runner_index)
        runner_dropdown.connect("changed", self.on_runner_changed)
        cell = Gtk.CellRendererText()
        cell.props.ellipsize = Pango.EllipsizeMode.END
        runner_dropdown.pack_start(cell, True)
        runner_dropdown.add_attribute(cell, 'text', 0)
        return runner_dropdown

    @staticmethod
    def _get_runner_liststore():
        """Build a ListStore with available runners."""
        runner_liststore = Gtk.ListStore(str, str)
        runner_liststore.append(("Select a runner from the list", ""))
        for runner in runners.get_installed():
            description = runner.description
            runner_liststore.append(
                ("%s (%s)" % (runner.name, description), runner.name))
        return runner_liststore

    def on_slug_change_clicked(self, widget):
        if self.slug_entry.get_sensitive() is False:
            self.slug_entry.set_sensitive(True)
        else:
            self.change_game_slug()

    def on_slug_entry_activate(self, widget):
        self.change_game_slug()

    def change_game_slug(self):
        self.slug = self.slug_entry.get_text()
        self.slug_entry.set_sensitive(False)

    def on_install_runners_clicked(self, _button):
        runners_dialog = gui.runnersdialog.RunnersDialog()
        runners_dialog.connect("runner-installed",
                               self._update_runner_dropdown)

    def _update_runner_dropdown(self, _widget):
        active_id = self.runner_dropdown.get_active_id()
        self.runner_dropdown.set_model(self._get_runner_liststore())
        self.runner_dropdown.set_active_id(active_id)

    def _build_game_tab(self):
        if self.game and self.runner_name:
            self.game.runner_name = self.runner_name
            try:
                self.game.runner = runners.import_runner(self.runner_name)
            except runners.InvalidRunner:
                pass
            self.game_box = GameBox(self.lutris_config, self.game)
            game_sw = self.build_scrolled_window(self.game_box)
        elif self.runner_name:
            game = Game(None)
            game.runner_name = self.runner_name
            self.game_box = GameBox(self.lutris_config, game)
            game_sw = self.build_scrolled_window(self.game_box)
        else:
            game_sw = Gtk.Label(label=self.no_runner_label)
        self._add_notebook_tab(game_sw, "Game options")

    def _build_runner_tab(self, config_level):
        if self.runner_name:
            self.runner_box = RunnerBox(self.lutris_config)
            runner_sw = self.build_scrolled_window(self.runner_box)
        else:
            runner_sw = Gtk.Label(label=self.no_runner_label)
        self._add_notebook_tab(runner_sw, "Runner options")

    def _build_system_tab(self, config_level):
        self.system_box = SystemBox(self.lutris_config)
        self.system_sw = self.build_scrolled_window(self.system_box)
        self._add_notebook_tab(self.system_sw, "System options")

    def _add_notebook_tab(self, widget, label):
        self.notebook.append_page(widget, Gtk.Label(label=label))

    def build_action_area(self, button_callback, callback2=None):
        self.action_area.set_layout(Gtk.ButtonBoxStyle.EDGE)

        # Advanced settings checkbox
        checkbox = Gtk.CheckButton(label="Show advanced options")
        value = settings.read_setting('show_advanced_options')
        if value == 'True':
            checkbox.set_active(value)
        checkbox.connect("toggled", self.on_show_advanced_options_toggled)
        self.action_area.pack_start(checkbox, False, False, 5)

        # Buttons
        hbox = Gtk.HBox()
        cancel_button = Gtk.Button(label="Cancel")
        cancel_button.connect("clicked", self.on_cancel_clicked)
        hbox.pack_start(cancel_button, True, True, 10)

        save_button = Gtk.Button(label="Save")
        if callback2:
            save_button.connect("clicked", button_callback, callback2)
        else:
            save_button.connect("clicked", button_callback)
        hbox.pack_start(save_button, True, True, 0)
        self.action_area.pack_start(hbox, True, True, 0)

    def on_show_advanced_options_toggled(self, checkbox):
        value = True if checkbox.get_active() else False
        settings.write_setting('show_advanced_options', value)

        self._set_advanced_options_visible(value)

    def _set_advanced_options_visible(self, value):
        """Change visibility of advanced options across all config tabs."""
        widgets = self.system_box.get_children()
        if self.runner_name:
            widgets += self.runner_box.get_children()
        if self.game:
            widgets += self.game_box.get_children()

        for widget in widgets:
            if widget.get_style_context().has_class('advanced'):
                widget.set_visible(value)
                if value:
                    widget.set_no_show_all(not value)
                    widget.show_all()

    def on_runner_changed(self, widget):
        """Action called when runner drop down is changed."""
        runner_index = widget.get_active()
        current_page = self.notebook.get_current_page()

        if runner_index == 0:
            self.runner_name = None
            self.lutris_config = LutrisConfig()
        else:
            self.runner_name = widget.get_model()[runner_index][1]
            self.lutris_config = LutrisConfig(
                runner_slug=self.runner_name,
                game_config_id=self.game_config_id,
                level='game')

        self._rebuild_tabs()
        self.notebook.set_current_page(current_page)

    def _rebuild_tabs(self):
        for i in range(self.notebook.get_n_pages(), 1, -1):
            self.notebook.remove_page(i - 1)
        self._build_game_tab()
        self._build_runner_tab('game')
        self._build_system_tab('game')
        self.show_all()

    def on_cancel_clicked(self, widget=None):
        """Dialog destroy callback."""
        self.destroy()

    def is_valid(self):
        name = self.name_entry.get_text()
        if not self.runner_name:
            ErrorDialog("Runner not provided")
            return False
        if not name:
            ErrorDialog("Please fill in the name")
            return False
        return True

    def on_save(self, _button, callback=None):
        """Save game info and destroy widget. Return True if success."""
        if not self.is_valid():
            return False
        name = self.name_entry.get_text()

        if not self.slug:
            self.slug = slugify(name)

        if not self.game:
            self.game = Game()

        if self.lutris_config.game_config_id == TEMP_CONFIG:
            self.lutris_config.game_config_id = self.get_config_id()

        runner_class = runners.import_runner(self.runner_name)
        runner = runner_class(self.lutris_config)
        self.game.name = name
        self.game.slug = self.slug
        self.game.runner_name = self.runner_name
        self.game.config = self.lutris_config
        self.game.directory = runner.game_path
        self.game.is_installed = True
        if self.runner_name in ('steam', 'winesteam'):
            self.game.steamid = self.lutris_config.game_config['appid']
        self.game.save()
        self.destroy()
        logger.debug("Saved %s", name)
        self.saved = True
        if callback:
            callback()

    def on_custom_image_select(self, widget, image_type):
        dialog = Gtk.FileChooserDialog(
            "Please choose a custom image", self, Gtk.FileChooserAction.OPEN,
            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN,
             Gtk.ResponseType.OK))

        image_filter = Gtk.FileFilter()
        image_filter.set_name("Images")
        image_filter.add_pixbuf_formats()
        dialog.add_filter(image_filter)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            image_path = dialog.get_filename()
            if image_type == 'banner':
                self.game.has_custom_banner = True
                dest_path = datapath.get_banner_path(self.game.slug)
                size = BANNER_SIZE
                file_format = 'jpeg'
            else:
                self.game.has_custom_icon = True
                dest_path = datapath.get_icon_path(self.game.slug)
                size = ICON_SIZE
                file_format = 'png'
            pixbuf = get_pixbuf(image_path, None, size)
            pixbuf.savev(dest_path, file_format, [], [])
            self._set_image(image_type)

        dialog.destroy()

    def on_custom_image_reset_clicked(self, widget, image_type):
        if image_type == 'banner':
            self.game.has_custom_banner = False
            dest_path = datapath.get_banner_path(self.game.slug)
        elif image_type == 'icon':
            self.game.has_custom_icon = False
            dest_path = datapath.get_icon_path(self.game.slug)
        else:
            raise ValueError('Unsupported image type %s', image_type)
        os.remove(dest_path)
        self._set_image(image_type)
Ejemplo n.º 38
0
class GameDialogCommon(object):
    no_runner_label = "Select a runner from the list"

    @staticmethod
    def get_runner_liststore():
        """Build a ListStore with available runners."""
        runner_liststore = Gtk.ListStore(str, str)
        runner_liststore.append(("Select a runner from the list", ""))
        for runner_name in runners.__all__:
            runner_class = runners.import_runner(runner_name)
            runner = runner_class()
            if runner.is_installed():
                description = runner.description
                runner_liststore.append(
                    ("%s (%s)" % (runner_name, description), runner_name)
                )
        return runner_liststore

    def build_entry_box(self, entry, label_text=None):
        box = Gtk.HBox()
        if label_text:
            label = Gtk.Label(label=label_text)
            box.pack_start(label, False, False, 20)
        box.pack_start(entry, True, True, 20)
        return box

    def get_runner_dropdown(self):
        runner_liststore = self.get_runner_liststore()
        self.runner_dropdown = Gtk.ComboBox.new_with_model(runner_liststore)
        runner_index = 0
        if self.game:
            for runner in runner_liststore:
                if self.runner_name == str(runner[1]):
                    break
                runner_index += 1
        self.runner_dropdown.set_active(runner_index)
        self.runner_dropdown.connect("changed", self.on_runner_changed)
        cell = Gtk.CellRendererText()
        cell.props.ellipsize = Pango.EllipsizeMode.END
        self.runner_dropdown.pack_start(cell, True)
        self.runner_dropdown.add_attribute(cell, 'text', 0)
        return self.runner_dropdown

    @staticmethod
    def build_scrolled_window(widget):
        scrolled_window = Gtk.ScrolledWindow()
        scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC,
                                   Gtk.PolicyType.AUTOMATIC)
        scrolled_window.add_with_viewport(widget)
        return scrolled_window

    def build_notebook(self):
        self.notebook = Gtk.Notebook()
        self.vbox.pack_start(self.notebook, True, True, 10)

    def add_notebook_tab(self, widget, label):
        self.notebook.append_page(widget, Gtk.Label(label=label))

    def build_info_tab(self):
        info_box = VBox()
        self.name_entry = Gtk.Entry()
        if self.game:
            self.name_entry.set_text(self.game.name)
        name_box = self.build_entry_box(self.name_entry, "Name")
        info_box.pack_start(name_box, False, False, 5)

        if self.game:
            self.slug_entry = Gtk.Entry()
            self.slug_entry.set_text(self.game.slug)
            self.slug_entry.set_sensitive(False)
            slug_box = self.build_entry_box(self.slug_entry, "Identifier")
            info_box.pack_start(slug_box, False, False, 5)

        runner_box = Gtk.HBox()
        label = Gtk.Label("Runner")
        label.set_alignment(0.5, 0.5)
        runner_dropdown = self.get_runner_dropdown()
        runner_box.pack_start(label, False, False, 20)
        runner_box.pack_start(runner_dropdown, False, False, 20)
        info_box.pack_start(runner_box, False, False, 5)

        info_sw = self.build_scrolled_window(info_box)
        self.add_notebook_tab(info_sw, "Game info")

    def build_game_tab(self):
        if self.game and self.runner_name:
            self.game.runner_name = self.runner_name
            self.game_box = GameBox(self.lutris_config, self.game)
            game_sw = self.build_scrolled_window(self.game_box)
        elif self.runner_name:
            game = Game(None)
            game.runner_name = self.runner_name
            self.game_box = GameBox(self.lutris_config, game)
            game_sw = self.build_scrolled_window(self.game_box)
        else:
            game_sw = Gtk.Label(label=self.no_runner_label)
        self.add_notebook_tab(game_sw, "Game options")

    def build_runner_tab(self, config_level):
        if self.runner_name:
            self.runner_box = RunnerBox(self.lutris_config)
            runner_sw = self.build_scrolled_window(self.runner_box)
        else:
            runner_sw = Gtk.Label(label=self.no_runner_label)
        self.add_notebook_tab(runner_sw, "Runner options")

    def build_system_tab(self, config_level):
        self.system_box = SystemBox(self.lutris_config)
        self.system_sw = self.build_scrolled_window(self.system_box)
        self.add_notebook_tab(self.system_sw, "System options")

    def build_tabs(self, config_level):
        if config_level == 'game':
            self.build_info_tab()
            self.build_game_tab()
        self.build_runner_tab(config_level)
        self.build_system_tab(config_level)

    def rebuild_tabs(self):
        for i in range(self.notebook.get_n_pages(), 1, -1):
            self.notebook.remove_page(i - 1)
        self.build_game_tab()
        self.build_runner_tab('game')
        self.build_system_tab('game')
        self.show_all()

    def build_action_area(self, label, button_callback):
        self.action_area.set_layout(Gtk.ButtonBoxStyle.EDGE)

        # Advanced settings checkbox
        checkbox = Gtk.CheckButton(label="Show advanced options")
        value = settings.read_setting('show_advanced_options')
        if value == 'True':
            checkbox.set_active(value)
        checkbox.connect("toggled", self.on_show_advanced_options_toggled)
        self.action_area.pack_start(checkbox, False, False, 5)

        # Buttons
        hbox = Gtk.HBox()
        cancel_button = Gtk.Button(label="Cancel")
        cancel_button.connect("clicked", self.on_cancel_clicked)
        hbox.pack_start(cancel_button, True, True, 10)

        button = Gtk.Button(label=label)
        button.connect("clicked", button_callback)
        hbox.pack_start(button, True, True, 0)
        self.action_area.pack_start(hbox, True, True, 0)

    def set_advanced_options_visible(self, value):
        """Change visibility of advanced options across all config tabs."""
        widgets = self.system_box.get_children()
        if self.runner_name:
            widgets += self.runner_box.get_children()
        if self.game:
            widgets += self.game_box.get_children()

        for widget in widgets:
            if widget.get_style_context().has_class('advanced'):
                widget.set_visible(value)
                if value:
                    widget.set_no_show_all(not value)
                    widget.show_all()

    def on_show_advanced_options_toggled(self, checkbox):
        value = True if checkbox.get_active() else False
        settings.write_setting('show_advanced_options', value)

        self.set_advanced_options_visible(value)

    def on_runner_changed(self, widget):
        """Action called when runner drop down is changed."""
        runner_index = widget.get_active()
        current_page = self.notebook.get_current_page()

        if runner_index == 0:
            self.runner_name = None
            self.lutris_config = LutrisConfig()
        else:
            self.runner_name = widget.get_model()[runner_index][1]
            # XXX DANGER ZONE
            self.lutris_config = LutrisConfig(runner_slug=self.runner_name,
                                              level='game')

        self.rebuild_tabs()
        self.notebook.set_current_page(current_page)

    def on_cancel_clicked(self, widget=None):
        """Dialog destroy callback."""
        self.destroy()

    def is_valid(self):
        name = self.name_entry.get_text()
        if not self.runner_name:
            ErrorDialog("Runner not provided")
            return False
        if not name:
            ErrorDialog("Please fill in the name")
            return False
        return True

    def on_save(self, _button):
        """Save game info and destroy widget. Return True if success."""
        if not self.is_valid():
            return False
        name = self.name_entry.get_text()

        # Do not modify slug
        if not self.slug:
            self.slug = slugify(name)

        if not self.game:
            self.game = Game(self.slug)
            self.game.config = self.lutris_config

        if not self.lutris_config.game_slug:
            self.lutris_config.game_slug = self.slug
        self.lutris_config.save()

        runner_class = runners.import_runner(self.runner_name)
        runner = runner_class(self.lutris_config)
        self.game.name = name
        self.game.slug = self.slug
        self.game.runner_name = self.runner_name
        self.game.directory = runner.game_path
        self.game.is_installed = True
        self.game.save()
        self.destroy()
        logger.debug("Saved %s", name)
        self.saved = True
Ejemplo n.º 39
0
class GameDialogCommon:
    """Mixin for config dialogs"""
    no_runner_label = _("Select a runner in the Game Info tab")

    def __init__(self):
        self.notebook = None
        self.vbox = None
        self.action_area = None
        self.name_entry = None
        self.runner_box = None

        self.timer_id = None
        self.game = None
        self.saved = None
        self.slug = None
        self.slug_entry = None
        self.directory_entry = None
        self.year_entry = None
        self.slug_change_button = None
        self.runner_dropdown = None
        self.banner_button = None
        self.icon_button = None
        self.game_box = None
        self.system_box = None
        self.system_sw = None
        self.runner_name = None
        self.runner_index = None
        self.lutris_config = None
        self.clipboard = None
        self._clipboard_buffer = None

    @staticmethod
    def build_scrolled_window(widget):
        """Return a scrolled window for containing config widgets"""
        scrolled_window = Gtk.ScrolledWindow()
        scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC,
                                   Gtk.PolicyType.AUTOMATIC)
        scrolled_window.add(widget)
        return scrolled_window

    def build_notebook(self):
        self.notebook = Gtk.Notebook()
        self.vbox.pack_start(self.notebook, True, True, 10)

    def build_tabs(self, config_level):
        self.timer_id = None
        if config_level == "game":
            self._build_info_tab()
            self._build_game_tab()
        if config_level in ("game", "runner"):
            self._build_runner_tab(config_level)
        if config_level == "system":
            self._build_prefs_tab()
            self._build_sysinfo_tab()
        self._build_system_tab(config_level)

    def _build_info_tab(self):
        info_box = VBox()

        if self.game:
            info_box.pack_start(self._get_banner_box(), False, False,
                                6)  # Banner

        info_box.pack_start(self._get_name_box(), False, False, 6)  # Game name

        self.runner_box = self._get_runner_box()
        info_box.pack_start(self.runner_box, False, False, 6)  # Runner

        info_box.pack_start(self._get_year_box(), False, False, 6)  # Year

        if self.game:
            info_box.pack_start(self._get_slug_box(), False, False, 6)
            info_box.pack_start(self._get_directory_box(), False, False, 6)

        info_sw = self.build_scrolled_window(info_box)
        self._add_notebook_tab(info_sw, _("Game info"))

    def _build_prefs_tab(self):
        prefs_box = VBox()
        settings_options = {
            "hide_client_on_game_start":
            _("Minimize client when a game is launched"),
            "hide_text_under_icons":
            _("Hide text under icons"),
            "show_tray_icon":
            _("Show Tray Icon"),
        }
        for setting_key, label in settings_options.items():
            prefs_box.pack_start(self._get_setting_box(setting_key, label),
                                 False, False, 6)
        info_sw = self.build_scrolled_window(prefs_box)
        self._add_notebook_tab(info_sw, _("Lutris preferences"))

    def _build_sysinfo_tab(self):
        sysinfo_box = Gtk.VBox()
        sysinfo_view = LogTextView()
        sysinfo_view.set_cursor_visible(False)
        sysinfo_str = gather_system_info_str()

        text_buffer = sysinfo_view.get_buffer()
        text_buffer.set_text(sysinfo_str)
        self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        self._clipboard_buffer = sysinfo_str

        button_copy = Gtk.Button(_("Copy System Info"))
        button_copy.connect("clicked", self._copy_text)

        sysinfo_box.add(sysinfo_view)
        sysinfo_box.add(button_copy)
        info_sw = self.build_scrolled_window(sysinfo_box)
        self._add_notebook_tab(info_sw, _("System Information"))

    def _copy_text(self, widget):  # pylint: disable=unused-argument
        self.clipboard.set_text(self._clipboard_buffer, -1)

    def _get_setting_box(self, setting_key, label):
        box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)
        checkbox = Gtk.CheckButton(label=label)
        if settings.read_setting(setting_key).lower() == "true":
            checkbox.set_active(True)
        checkbox.connect("toggled", self._on_setting_change, setting_key)
        box.pack_start(checkbox, True, True, 0)
        return box

    def _on_setting_change(self, widget, setting_key):
        """Save a setting when an option is toggled"""
        settings.write_setting(setting_key, widget.get_active())

    def _get_name_box(self):
        box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)
        label = Label(_("Name"))
        box.pack_start(label, False, False, 0)
        self.name_entry = Gtk.Entry()
        if self.game:
            self.name_entry.set_text(self.game.name)
        box.pack_start(self.name_entry, True, True, 0)
        return box

    def _get_slug_box(self):
        slug_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)

        label = Label(_("Identifier"))
        slug_box.pack_start(label, False, False, 0)

        self.slug_entry = SlugEntry()
        self.slug_entry.set_text(self.game.slug)
        self.slug_entry.set_sensitive(False)
        self.slug_entry.connect("activate", self.on_slug_entry_activate)
        slug_box.pack_start(self.slug_entry, True, True, 0)

        self.slug_change_button = Gtk.Button(_("Change"))
        self.slug_change_button.connect("clicked", self.on_slug_change_clicked)
        slug_box.pack_start(self.slug_change_button, False, False, 0)

        return slug_box

    def _get_directory_box(self):
        """Return widget displaying the location of the game and allowing to move it"""
        box = Gtk.Box(spacing=12,
                      margin_right=12,
                      margin_left=12,
                      visible=True)
        label = Label(_("Directory"))
        box.pack_start(label, False, False, 0)
        self.directory_entry = Gtk.Entry(visible=True)
        self.directory_entry.set_text(self.game.directory)
        self.directory_entry.set_sensitive(False)
        box.pack_start(self.directory_entry, True, True, 0)
        move_button = Gtk.Button(_("Move"), visible=True)
        move_button.connect("clicked", self.on_move_clicked)
        box.pack_start(move_button, False, False, 0)
        return box

    def _get_runner_box(self):
        runner_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)

        runner_label = Label(_("Runner"))
        runner_box.pack_start(runner_label, False, False, 0)

        self.runner_dropdown = self._get_runner_dropdown()
        runner_box.pack_start(self.runner_dropdown, True, True, 0)

        install_runners_btn = Gtk.Button(_("Install runners"))
        install_runners_btn.connect("clicked", self.on_install_runners_clicked)
        runner_box.pack_start(install_runners_btn, True, True, 0)

        return runner_box

    def _get_banner_box(self):
        banner_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)

        label = Label("")
        banner_box.pack_start(label, False, False, 0)

        self.banner_button = Gtk.Button()
        self._set_image("banner")
        self.banner_button.connect("clicked", self.on_custom_image_select,
                                   "banner")
        banner_box.pack_start(self.banner_button, False, False, 0)

        reset_banner_button = Gtk.Button.new_from_icon_name(
            "edit-clear", Gtk.IconSize.MENU)
        reset_banner_button.set_relief(Gtk.ReliefStyle.NONE)
        reset_banner_button.set_tooltip_text(_("Remove custom banner"))
        reset_banner_button.connect("clicked",
                                    self.on_custom_image_reset_clicked,
                                    "banner")
        banner_box.pack_start(reset_banner_button, False, False, 0)

        self.icon_button = Gtk.Button()
        self._set_image("icon")
        self.icon_button.connect("clicked", self.on_custom_image_select,
                                 "icon")
        banner_box.pack_start(self.icon_button, False, False, 0)

        reset_icon_button = Gtk.Button.new_from_icon_name(
            "edit-clear", Gtk.IconSize.MENU)
        reset_icon_button.set_relief(Gtk.ReliefStyle.NONE)
        reset_icon_button.set_tooltip_text(_("Remove custom icon"))
        reset_icon_button.connect("clicked",
                                  self.on_custom_image_reset_clicked, "icon")
        banner_box.pack_start(reset_icon_button, False, False, 0)

        return banner_box

    def _get_year_box(self):
        box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)

        label = Label(_("Release year"))
        box.pack_start(label, False, False, 0)

        self.year_entry = NumberEntry()
        if self.game:
            self.year_entry.set_text(str(self.game.year or ""))
        box.pack_start(self.year_entry, True, True, 0)

        return box

    def _set_image(self, image_format):
        image = Gtk.Image()
        size = BANNER_SIZE if image_format == "banner" else ICON_SIZE
        game_slug = self.game.slug if self.game else ""
        image.set_from_pixbuf(get_pixbuf_for_game(game_slug, size))
        if image_format == "banner":
            self.banner_button.set_image(image)
        else:
            self.icon_button.set_image(image)

    def _get_runner_dropdown(self):
        runner_liststore = self._get_runner_liststore()
        runner_dropdown = Gtk.ComboBox.new_with_model(runner_liststore)
        runner_dropdown.set_id_column(1)
        runner_index = 0
        if self.runner_name:
            for runner in runner_liststore:
                if self.runner_name == str(runner[1]):
                    break
                runner_index += 1
        self.runner_index = runner_index
        runner_dropdown.set_active(self.runner_index)
        runner_dropdown.connect("changed", self.on_runner_changed)
        cell = Gtk.CellRendererText()
        cell.props.ellipsize = Pango.EllipsizeMode.END
        runner_dropdown.pack_start(cell, True)
        runner_dropdown.add_attribute(cell, "text", 0)
        return runner_dropdown

    @staticmethod
    def _get_runner_liststore():
        """Build a ListStore with available runners."""
        runner_liststore = Gtk.ListStore(str, str)
        runner_liststore.append((_("Select a runner from the list"), ""))
        for runner in runners.get_installed():
            description = runner.description
            runner_liststore.append(
                ("%s (%s)" % (runner.human_name, description), runner.name))
        return runner_liststore

    def on_slug_change_clicked(self, widget):
        if self.slug_entry.get_sensitive() is False:
            widget.set_label(_("Apply"))
            self.slug_entry.set_sensitive(True)
        else:
            self.change_game_slug()

    def on_slug_entry_activate(self, _widget):
        self.change_game_slug()

    def change_game_slug(self):
        self.slug = self.slug_entry.get_text()
        self.slug_entry.set_sensitive(False)
        self.slug_change_button.set_label(_("Change"))

    def on_move_clicked(self, _button):
        new_location = DirectoryDialog("Select new location for the game",
                                       default_path=self.game.directory)
        new_directory = self.game.move(new_location.folder)
        if new_directory:
            self.directory_entry.set_text(new_directory)

    def on_install_runners_clicked(self, _button):
        """Messed up callback requiring an import in the method to avoid a circular dependency"""
        from lutris.gui.dialogs.runners import RunnersDialog
        runners_dialog = RunnersDialog()
        runners_dialog.connect("runner-installed", self.on_runner_installed)

    def on_runner_installed(self, _dialog):
        """Callback triggered when new runners are installed"""
        active_id = self.runner_dropdown.get_active_id()
        self.runner_dropdown.set_model(self._get_runner_liststore())
        self.runner_dropdown.set_active_id(active_id)

    def _build_game_tab(self):
        if self.game and self.runner_name:
            self.game.runner_name = self.runner_name
            if not self.game.runner or self.game.runner.name != self.runner_name:
                try:
                    self.game.runner = runners.import_runner(
                        self.runner_name)()
                except runners.InvalidRunner:
                    pass
            self.game_box = GameBox(self.lutris_config, self.game)
            game_sw = self.build_scrolled_window(self.game_box)
        elif self.runner_name:
            game = Game(None)
            game.runner_name = self.runner_name
            self.game_box = GameBox(self.lutris_config, game)
            game_sw = self.build_scrolled_window(self.game_box)
        else:
            game_sw = Gtk.Label(label=self.no_runner_label)
        self._add_notebook_tab(game_sw, _("Game options"))

    def _build_runner_tab(self, _config_level):
        if self.runner_name:
            self.runner_box = RunnerBox(self.lutris_config, self.game)
            runner_sw = self.build_scrolled_window(self.runner_box)
        else:
            runner_sw = Gtk.Label(label=self.no_runner_label)
        self._add_notebook_tab(runner_sw, _("Runner options"))

    def _build_system_tab(self, _config_level):
        if not self.lutris_config:
            raise RuntimeError("Lutris config not loaded yet")
        self.system_box = SystemBox(self.lutris_config)
        self.system_sw = self.build_scrolled_window(self.system_box)
        self._add_notebook_tab(self.system_sw, _("System options"))

    def _add_notebook_tab(self, widget, label):
        self.notebook.append_page(widget, Gtk.Label(label=label))

    def build_action_area(self, button_callback):
        self.action_area.set_layout(Gtk.ButtonBoxStyle.EDGE)

        # Advanced settings checkbox
        checkbox = Gtk.CheckButton(label=_("Show advanced options"))
        if settings.read_setting("show_advanced_options") == "True":
            checkbox.set_active(True)
        checkbox.connect("toggled", self.on_show_advanced_options_toggled)
        self.action_area.pack_start(checkbox, False, False, 5)

        # Buttons
        hbox = Gtk.Box()
        cancel_button = Gtk.Button(label=_("Cancel"))
        cancel_button.connect("clicked", self.on_cancel_clicked)
        hbox.pack_start(cancel_button, True, True, 10)

        save_button = Gtk.Button(label=_("Save"))
        save_button.connect("clicked", button_callback)
        hbox.pack_start(save_button, True, True, 0)
        self.action_area.pack_start(hbox, True, True, 0)

    def on_show_advanced_options_toggled(self, checkbox):
        value = bool(checkbox.get_active())
        settings.write_setting("show_advanced_options", value)

        self._set_advanced_options_visible(value)

    def _set_advanced_options_visible(self, value):
        """Change visibility of advanced options across all config tabs."""
        widgets = self.system_box.get_children()
        if self.runner_name:
            widgets += self.runner_box.get_children()
        if self.game:
            widgets += self.game_box.get_children()

        for widget in widgets:
            if widget.get_style_context().has_class("advanced"):
                widget.set_visible(value)
                if value:
                    widget.set_no_show_all(not value)
                    widget.show_all()

    def on_runner_changed(self, widget):
        """Action called when runner drop down is changed."""
        new_runner_index = widget.get_active()
        if self.runner_index and new_runner_index != self.runner_index:
            dlg = QuestionDialog({
                "question":
                _("Are you sure you want to change the runner for this game ? "
                  "This will reset the full configuration for this game and "
                  "is not reversible."),
                "title":
                _("Confirm runner change"),
            })

            if dlg.result == Gtk.ResponseType.YES:
                self.runner_index = new_runner_index
                self._switch_runner(widget)
            else:
                # Revert the dropdown menu to the previously selected runner
                widget.set_active(self.runner_index)
        else:
            self.runner_index = new_runner_index
            self._switch_runner(widget)

    def _switch_runner(self, widget):
        """Rebuilds the UI on runner change"""
        current_page = self.notebook.get_current_page()
        if self.runner_index == 0:
            logger.info("No runner selected, resetting configuration")
            self.runner_name = None
            self.lutris_config = None
        else:
            runner_name = widget.get_model()[self.runner_index][1]
            if runner_name == self.runner_name:
                logger.debug("Runner unchanged, not creating a new config")
                return
            logger.info("Creating new configuration with runner %s",
                        runner_name)
            self.runner_name = runner_name
            self.lutris_config = LutrisConfig(runner_slug=self.runner_name,
                                              level="game")
        self._rebuild_tabs()
        self.notebook.set_current_page(current_page)

    def _rebuild_tabs(self):
        for i in range(self.notebook.get_n_pages(), 1, -1):
            self.notebook.remove_page(i - 1)
        self._build_game_tab()
        self._build_runner_tab("game")
        self._build_system_tab("game")
        self.show_all()

    def on_cancel_clicked(self, _widget=None, _event=None):
        """Dialog destroy callback."""
        if self.game:
            self.game.load_config()
        self.destroy()

    def is_valid(self):
        if not self.runner_name:
            ErrorDialog(_("Runner not provided"))
            return False
        if not self.name_entry.get_text():
            ErrorDialog(_("Please fill in the name"))
            return False
        if (self.runner_name in ("steam", "winesteam")
                and self.lutris_config.game_config.get("appid") is None):
            ErrorDialog(_("Steam AppId not provided"))
            return False
        invalid_fields = []
        runner_class = import_runner(self.runner_name)
        runner_instance = runner_class()
        for config in ["game", "runner"]:
            for k, v in getattr(self.lutris_config,
                                config + "_config").items():
                option = runner_instance.find_option(config + "_options", k)
                if option is None:
                    continue
                validator = option.get("validator")
                if validator is not None:
                    try:
                        res = validator(v)
                        logger.debug("%s validated successfully: %s", k, res)
                    except Exception:
                        invalid_fields.append(option.get("label"))
        if invalid_fields:
            ErrorDialog(
                _("The following fields have invalid values: ") +
                ", ".join(invalid_fields))
            return False
        return True

    def on_save(self, _button):
        """Save game info and destroy widget. Return True if success."""
        if not self.is_valid():
            logger.warning(
                _("Current configuration is not valid, ignoring save request"))
            return False
        name = self.name_entry.get_text()

        if not self.slug:
            self.slug = slugify(name)

        if not self.game:
            self.game = Game()

        year = None
        if self.year_entry.get_text():
            year = int(self.year_entry.get_text())

        if not self.lutris_config.game_config_id:
            self.lutris_config.game_config_id = make_game_config_id(self.slug)

        runner_class = runners.import_runner(self.runner_name)
        runner = runner_class(self.lutris_config)

        self.game.name = name
        self.game.slug = self.slug
        self.game.year = year
        self.game.game_config_id = self.lutris_config.game_config_id
        self.game.runner = runner
        self.game.runner_name = self.runner_name
        self.game.directory = runner.game_path
        self.game.is_installed = True
        self.game.config = self.lutris_config
        self.game.save(save_config=True)
        self.destroy()
        self.saved = True
        return True

    def on_custom_image_select(self, _widget, image_type):
        dialog = Gtk.FileChooserDialog(
            _("Please choose a custom image"),
            self,
            Gtk.FileChooserAction.OPEN,
            (
                Gtk.STOCK_CANCEL,
                Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OPEN,
                Gtk.ResponseType.OK,
            ),
        )

        image_filter = Gtk.FileFilter()
        image_filter.set_name(_("Images"))
        image_filter.add_pixbuf_formats()
        dialog.add_filter(image_filter)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            image_path = dialog.get_filename()
            if image_type == "banner":
                self.game.has_custom_banner = True
                dest_path = os.path.join(settings.BANNER_PATH,
                                         "%s.jpg" % self.game.slug)
                size = BANNER_SIZE
                file_format = "jpeg"
            else:
                self.game.has_custom_icon = True
                dest_path = resources.get_icon_path(self.game.slug)
                size = ICON_SIZE
                file_format = "png"
            pixbuf = get_pixbuf(image_path, size)
            pixbuf.savev(dest_path, file_format, [], [])
            self._set_image(image_type)

            if image_type == "icon":
                system.update_desktop_icons()

        dialog.destroy()

    def on_custom_image_reset_clicked(self, _widget, image_type):
        if image_type == "banner":
            self.game.has_custom_banner = False
            dest_path = os.path.join(settings.BANNER_PATH,
                                     "%s.jpg" % self.game.slug)
        elif image_type == "icon":
            self.game.has_custom_icon = False
            dest_path = resources.get_icon_path(self.game.slug)
        else:
            raise ValueError("Unsupported image type %s" % image_type)
        os.remove(dest_path)
        self._set_image(image_type)
Ejemplo n.º 40
0
 def on_game_selected(self, _widget, game_id):
     self.application.launch(Game(game_id))
Ejemplo n.º 41
0
class LutrisWindow(object):
    """Handler class for main window signals."""
    def __init__(self):

        ui_filename = os.path.join(datapath.get(), 'ui', 'LutrisWindow.ui')
        if not os.path.exists(ui_filename):
            raise IOError('File %s not found' % ui_filename)

        # Currently running game
        self.running_game = None

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

        self.builder = Gtk.Builder()
        self.builder.add_from_file(ui_filename)

        # load config
        width = int(settings.read_setting('width') or 800)
        height = int(settings.read_setting('height') or 600)
        self.window_size = (width, height)
        view_type = self.get_view_type()
        self.icon_type = self.get_icon_type(view_type)
        filter_installed_setting = settings.read_setting(
            'filter_installed') or 'false'
        self.filter_installed = filter_installed_setting == 'true'
        show_installed_games_menuitem = self.builder.get_object(
            'filter_installed')
        show_installed_games_menuitem.set_active(self.filter_installed)
        logger.debug("Getting game list")
        game_list = get_game_list(self.filter_installed)
        logger.debug("Switching view")
        self.view = load_view(view_type, game_list, icon_type=self.icon_type)
        logger.debug("Connecting signals")
        self.main_box = self.builder.get_object('main_box')
        self.splash_box = self.builder.get_object('splash_box')
        # View menu
        self.grid_view_menuitem = self.builder.get_object("gridview_menuitem")
        self.grid_view_menuitem.set_active(view_type == 'grid')
        self.list_view_menuitem = self.builder.get_object("listview_menuitem")
        self.list_view_menuitem.set_active(view_type == 'list')
        # View buttons
        self.grid_view_btn = self.builder.get_object('switch_grid_view_btn')
        self.grid_view_btn.set_active(view_type == 'grid')
        self.list_view_btn = self.builder.get_object('switch_list_view_btn')
        self.list_view_btn.set_active(view_type == 'list')
        # Icon type menu
        self.banner_small_menuitem = \
            self.builder.get_object('banner_small_menuitem')
        self.banner_small_menuitem.set_active(self.icon_type == 'banner_small')
        self.banner_menuitem = self.builder.get_object('banner_menuitem')
        self.banner_menuitem.set_active(self.icon_type == 'banner')
        self.icon_menuitem = self.builder.get_object('icon_menuitem')
        self.icon_menuitem.set_active(self.icon_type == 'grid')

        self.search_entry = self.builder.get_object('search_entry')

        # Scroll window
        self.games_scrollwindow = self.builder.get_object('games_scrollwindow')
        self.games_scrollwindow.add(self.view)
        # Status bar
        self.status_label = self.builder.get_object('status_label')
        self.joystick_icons = []
        # Buttons
        self.stop_button = self.builder.get_object('stop_button')
        self.stop_button.set_sensitive(False)
        self.delete_button = self.builder.get_object('delete_button')
        self.delete_button.set_sensitive(False)
        self.play_button = self.builder.get_object('play_button')
        self.play_button.set_sensitive(False)

        # Contextual menu
        menu_callbacks = [
            ('play', self.on_game_clicked),
            ('install', self.on_game_clicked),
            ('add', self.add_manually),
            ('configure', self.edit_game_configuration),
            ('browse', self.on_browse_files),
            ('desktop-shortcut', self.create_desktop_shortcut),
            ('menu-shortcut', self.create_menu_shortcut),
            ('remove', self.on_remove_game),
        ]
        self.menu = ContextualMenu(menu_callbacks)
        self.view.contextual_menu = self.menu

        # Timer
        self.timer_id = GLib.timeout_add(2000, self.refresh_status)

        # Window initialization
        self.window = self.builder.get_object("window")
        self.window.resize_to_geometry(width, height)
        self.window.show_all()
        self.builder.connect_signals(self)
        self.connect_signals()

        # XXX Hide PGA config menu item until it actually gets implemented
        pga_menuitem = self.builder.get_object('pga_menuitem')
        pga_menuitem.hide()

        self.switch_splash_screen()

        credentials = api.read_api_key()
        if credentials:
            self.on_connect_success(None, credentials)
        else:
            self.toggle_connection(False)
            async_call(self.sync_icons, None)

    @property
    def current_view_type(self):
        return 'grid' \
            if self.view.__class__.__name__ == "GameGridView" \
            else 'list'

    def switch_splash_screen(self):
        if self.view.n_games == 0:
            self.splash_box.show()
            self.games_scrollwindow.hide()
        else:
            self.splash_box.hide()
            self.games_scrollwindow.show()

    def sync_icons(self):
        game_list = pga.get_games()
        resources.fetch_icons([game_info['slug'] for game_info in game_list],
                              callback=self.on_image_downloaded)

    def connect_signals(self):
        """Connect signals from the view with the main window.
           This must be called each time the view is rebuilt.
        """
        self.view.connect('game-installed', self.on_game_installed)
        self.view.connect("game-activated", self.on_game_clicked)
        self.view.connect("game-selected", self.game_selection_changed)
        self.window.connect("configure-event", self.get_size)

    def get_view_type(self):
        view_type = settings.read_setting('view_type')
        if view_type in ['grid', 'list']:
            return view_type
        return settings.GAME_VIEW

    def get_icon_type(self, view_type):
        """Return the icon style depending on the type of view."""
        if view_type == 'list':
            icon_type = settings.read_setting('icon_type_listview')
            default = settings.ICON_TYPE_LISTVIEW
        else:
            icon_type = settings.read_setting('icon_type_gridview')
            default = settings.ICON_TYPE_GRIDVIEW
        if icon_type not in ("banner_small", "banner", "icon"):
            icon_type = default
        return icon_type

    def get_size(self, widget, _):
        self.window_size = widget.get_size()

    def refresh_status(self):
        """Refresh status bar."""
        if self.running_game:
            if hasattr(self.running_game.game_thread, "pid"):
                pid = self.running_game.game_thread.pid
                name = self.running_game.name
                if pid == 99999:
                    self.status_label.set_text("Preparing to launch %s" % name)
                elif pid is None:
                    self.status_label.set_text("Game has quit")
                else:
                    self.status_label.set_text("Playing %s (pid: %r)" %
                                               (name, pid))
        for index in range(4):
            self.joystick_icons.append(
                self.builder.get_object('js' + str(index) + 'image'))
            if os.path.exists("/dev/input/js%d" % index):
                self.joystick_icons[index].set_visible(True)
            else:
                self.joystick_icons[index].set_visible(False)
        return True

    def about(self, _widget, _data=None):
        """Open the about dialog."""
        dialogs.AboutDialog()

    def on_remove_game(self, _widget, _data=None):
        selected_game = self.view.selected_game
        UninstallGameDialog(slug=selected_game, callback=self.on_game_deleted)

    def on_game_deleted(self, game_slug, from_library=False):
        if from_library:
            self.view.remove_game(game_slug)
            self.switch_splash_screen()
        else:
            self.view.set_uninstalled(game_slug)

    # Callbacks
    def on_connect(self, *args):
        """Callback when a user connects to his account."""
        login_dialog = dialogs.ClientLoginDialog()
        login_dialog.connect('connected', self.on_connect_success)

    def on_disconnect(self, *args):
        api.disconnect()
        self.toggle_connection(False)

    def toggle_connection(self, is_connected, username=None):
        disconnect_menuitem = self.builder.get_object('disconnect_menuitem')
        connect_menuitem = self.builder.get_object('connect_menuitem')
        connection_label = self.builder.get_object('connection_label')

        if is_connected:
            disconnect_menuitem.show()
            connect_menuitem.hide()
            connection_status = "Connected as %s" % username
        else:
            disconnect_menuitem.hide()
            connect_menuitem.show()
            connection_status = "Not connected"
        logger.info(connection_status)
        connection_label.set_text(connection_status)

    def on_connect_success(self, dialog, credentials):
        if isinstance(credentials, str):
            username = credentials
        else:
            username = credentials["username"]
        self.toggle_connection(True, username)
        self.sync_library()

    def on_destroy(self, *args):
        """Signal for window close."""
        view_type = 'grid' if 'GridView' in str(type(self.view)) else 'list'
        settings.write_setting('view_type', view_type)
        width, height = self.window_size
        settings.write_setting('width', width)
        settings.write_setting('height', height)
        Gtk.main_quit(*args)
        logger.debug("Quitting lutris")

    def on_game_installed(self, view, slug):
        view.set_installed(Game(slug))

    def on_runners_activate(self, _widget, _data=None):
        """Callback when manage runners is activated."""
        RunnersDialog()

    def on_preferences_activate(self, _widget, _data=None):
        """Callback when preferences is activated."""
        SystemConfigDialog()

    def on_show_installed_games_toggled(self, widget, data=None):
        self.filter_installed = widget.get_active()
        setting_value = 'true' if self.filter_installed else 'false'
        settings.write_setting('filter_installed', setting_value)
        self.switch_view(self.current_view_type)

    def on_pga_menuitem_activate(self, _widget, _data=None):
        dialogs.PgaSourceDialog()

    def on_image_downloaded(self, game_slug):
        is_installed = Game(game_slug).is_installed
        self.view.update_image(game_slug, is_installed)

    def on_search_entry_changed(self, widget):
        self.view.emit('filter-updated', widget.get_text())

    def on_game_clicked(self, *args):
        """Launch a game."""
        # Wait two seconds to avoid running a game twice
        if time.time() - self.game_launch_time < 2:
            return
        self.game_launch_time = time.time()

        game_slug = self.view.selected_game
        if game_slug:
            self.running_game = Game(game_slug)
            if self.running_game.is_installed:
                self.stop_button.set_sensitive(True)
                self.running_game.play()
            else:
                InstallerDialog(game_slug, self)

    def set_status(self, text):
        self.status_label.set_text(text)

    def sync_library(self):
        def set_library_synced(result, error):
            self.set_status("Library synced")
            self.switch_splash_screen()

        self.set_status("Syncing library")
        async_call(
            api.sync,
            lambda r, e: async_call(self.sync_icons, set_library_synced),
            caller=self)

    def reset(self, *args):
        """Reset the desktop to it's initial state."""
        if self.running_game:
            self.running_game.quit_game()
            self.status_label.set_text("Stopped %s" % self.running_game.name)
            self.running_game = None
            self.stop_button.set_sensitive(False)

    def game_selection_changed(self, _widget):
        # Emulate double click to workaround GTK bug #484640
        # https://bugzilla.gnome.org/show_bug.cgi?id=484640
        if type(self.view) is GameGridView:
            is_double_click = time.time() - self.game_selection_time < 0.4
            is_same_game = self.view.selected_game == self.last_selected_game
            if is_double_click and is_same_game:
                self.on_game_clicked()
            self.game_selection_time = time.time()
            self.last_selected_game = self.view.selected_game

        sensitive = True if self.view.selected_game else False
        self.play_button.set_sensitive(sensitive)
        self.delete_button.set_sensitive(sensitive)

    def add_game_to_view(self, slug):
        if not slug:
            raise ValueError("Missing game slug")
        game = Game(slug)

        def do_add_game():
            self.view.add_game(game)
            self.switch_splash_screen()

        GLib.idle_add(do_add_game)

    def add_game(self, _widget, _data=None):
        """Add a new game."""
        add_game_dialog = AddGameDialog(self)
        add_game_dialog.run()
        if add_game_dialog.runner_name and add_game_dialog.slug:
            self.add_game_to_view(add_game_dialog.slug)

    def add_manually(self, *args):
        game = Game(self.view.selected_game)
        add_game_dialog = AddGameDialog(self, game)
        add_game_dialog.run()
        if add_game_dialog.installed:
            self.view.set_installed(game)

    def on_browse_files(self, widget):
        game = Game(self.view.selected_game)
        path = game.get_browse_dir()
        if path and os.path.exists(path):
            subprocess.Popen(['xdg-open', path])
        else:
            dialogs.NoticeDialog("Can't open %s \nThe folder doesn't exist." %
                                 path)

    def edit_game_configuration(self, _button):
        """Edit game preferences."""
        game = Game(self.view.selected_game)
        if game.is_installed:
            EditGameConfigDialog(self, game)

    def on_viewmenu_toggled(self, menuitem):
        view_type = 'grid' if menuitem.get_active() else 'list'
        if view_type == self.current_view_type:
            return
        self.switch_view(view_type)
        self.grid_view_btn.set_active(view_type == 'grid')
        self.list_view_btn.set_active(view_type == 'list')

    def on_viewbtn_toggled(self, widget):
        view_type = 'grid' if widget.get_active() else 'list'
        if view_type == self.current_view_type:
            return
        self.switch_view(view_type)
        self.grid_view_menuitem.set_active(view_type == 'grid')
        self.list_view_menuitem.set_active(view_type == 'list')

    def switch_view(self, view_type):
        """Switch between grid view and list view."""
        logger.debug("Switching view")
        self.icon_type = self.get_icon_type(view_type)
        self.view.destroy()
        self.view = load_view(
            view_type,
            get_game_list(filter_installed=self.filter_installed),
            filter_text=self.search_entry.get_text(),
            icon_type=self.icon_type)
        self.view.contextual_menu = self.menu
        self.connect_signals()
        self.games_scrollwindow.add(self.view)
        self.view.show_all()
        self.view.check_resize()
        # Note: set_active(True *or* False) apparently makes ALL the menuitems
        # in the group send the activate signal...
        if self.icon_type == 'banner_small':
            self.banner_small_menuitem.set_active(True)
        if self.icon_type == 'icon':
            self.icon_menuitem.set_active(True)
        if self.icon_type == 'banner':
            self.banner_menuitem.set_active(True)

    def on_icon_type_activate(self, menuitem):
        icon_type = menuitem.get_name()
        if icon_type == self.view.icon_type or not menuitem.get_active():
            return
        if self.current_view_type == 'grid':
            settings.write_setting('icon_type_gridview', icon_type)
        elif self.current_view_type == 'list':
            settings.write_setting('icon_type_listview', icon_type)
        self.switch_view(self.current_view_type)

    def create_menu_shortcut(self, *args):
        """Add the game to the system's Games menu."""
        game_slug = slugify(self.view.selected_game)
        create_launcher(game_slug, menu=True)
        dialogs.NoticeDialog(
            "Shortcut added to the Games category of the global menu.")

    def create_desktop_shortcut(self, *args):
        """Add the game to the system's Games menu."""
        game_slug = slugify(self.view.selected_game)
        create_launcher(game_slug, desktop=True)
        dialogs.NoticeDialog('Shortcut created on your desktop.')
Ejemplo n.º 42
0
    def _write_config(self):
        """Write the game configuration in the DB and config file.

        This needs to be unfucked
        """
        if self.extends:
            logger.info(
                "This is an extension to %s, not creating a new game entry",
                self.extends,
            )
            return
        configpath = make_game_config_id(self.slug)
        config_filename = os.path.join(settings.CONFIG_DIR, "games/%s.yml" % configpath)

        if self.requires:
            # Load the base game config
            required_game = pga.get_game_by_field(self.requires, field="installer_slug")
            base_config = LutrisConfig(
                runner_slug=self.runner, game_config_id=required_game["configpath"]
            )
            config = base_config.game_level
        else:
            config = {"game": {}}

        self.game_id = pga.add_or_update(
            name=self.game_name,
            runner=self.runner,
            slug=self.game_slug,
            directory=self.target_path,
            installed=1,
            installer_slug=self.slug,
            parent_slug=self.requires,
            year=self.year,
            steamid=self.steamid,
            configpath=configpath,
            id=self.game_id,
        )

        game = Game(self.game_id)
        game.set_platform_from_runner()
        game.save()

        logger.debug("Saved game entry %s (%d)", self.game_slug, self.game_id)

        # Config update
        if "system" in self.script:
            config["system"] = self._substitute_config(self.script["system"])
        if self.runner in self.script and self.script[self.runner]:
            config[self.runner] = self._substitute_config(self.script[self.runner])

        # Game options such as exe or main_file can be added at the root of the
        # script as a shortcut, this integrates them into the game config
        # properly
        launcher, launcher_value = _get_game_launcher(self.script)
        if isinstance(launcher_value, list):
            game_files = []
            for game_file in launcher_value:
                if game_file in self.game_files:
                    game_files.append(self.game_files[game_file])
                else:
                    game_files.append(game_file)
            config["game"][launcher] = game_files
        elif launcher_value:
            if launcher_value in self.game_files:
                launcher_value = self.game_files[launcher_value]
            elif self.target_path and os.path.exists(
                    os.path.join(self.target_path, launcher_value)
            ):
                launcher_value = os.path.join(self.target_path, launcher_value)
            config["game"][launcher] = launcher_value

        if "game" in self.script:
            try:
                config["game"].update(self.script["game"])
            except ValueError:
                raise ScriptingError("Invalid 'game' section", self.script["game"])
            config["game"] = self._substitute_config(config["game"])

        yaml_config = yaml.safe_dump(config, default_flow_style=False)
        with open(config_filename, "w") as config_file:
            config_file.write(yaml_config)
        if not self.extends:
            game.emit("game-installed")
Ejemplo n.º 43
0
 def create_menu_shortcut(self, *args):
     """Add the selected game to the system's Games menu."""
     game = Game(self.view.selected_game)
     xdg.create_launcher(game.slug, game.id, game.name, menu=True)
Ejemplo n.º 44
0
    def do_command_line(self, command_line):  # noqa: C901  # pylint: disable=arguments-differ
        # pylint: disable=too-many-locals,too-many-return-statements,too-many-branches
        # pylint: disable=too-many-statements
        # TODO: split into multiple methods to reduce complexity (35)
        options = command_line.get_options_dict()

        # Use stdout to output logs, only if no command line argument is
        # provided
        argc = len(sys.argv) - 1
        if "-d" in sys.argv or "--debug" in sys.argv:
            argc -= 1
        if not argc:
            # Switch back the log output to stderr (the default in Python)
            # to avoid messing with any output from command line options.

            # Use when targetting Python 3.7 minimum
            # console_handler.setStream(sys.stderr)

            # Until then...
            logger.removeHandler(log.console_handler)
            log.console_handler = logging.StreamHandler(stream=sys.stdout)
            log.console_handler.setFormatter(log.SIMPLE_FORMATTER)
            logger.addHandler(log.console_handler)

        # Set up logger
        if options.contains("debug"):
            log.console_handler.setFormatter(log.DEBUG_FORMATTER)
            logger.setLevel(logging.DEBUG)

        # Text only commands

        # Print Lutris version and exit
        if options.contains("version"):
            executable_name = os.path.basename(sys.argv[0])
            print(executable_name + "-" + settings.VERSION)
            logger.setLevel(logging.NOTSET)
            return 0

        logger.info("Lutris %s", settings.VERSION)
        migrate()
        run_all_checks()
        AsyncCall(init_dxvk_versions, None)

        # List game
        if options.contains("list-games"):
            game_list = games_db.get_games()
            if options.contains("installed"):
                game_list = [game for game in game_list if game["installed"]]
            if options.contains("json"):
                self.print_game_json(command_line, game_list)
            else:
                self.print_game_list(command_line, game_list)
            return 0
        # List Steam games
        if options.contains("list-steam-games"):
            self.print_steam_list(command_line)
            return 0
        # List Steam folders
        if options.contains("list-steam-folders"):
            self.print_steam_folders(command_line)
            return 0

        # Execute command in Lutris context
        if options.contains("exec"):
            command = options.lookup_value("exec").get_string()
            self.execute_command(command)
            return 0

        if options.contains("submit-issue"):
            IssueReportWindow(application=self)
            return 0

        try:
            url = options.lookup_value(GLib.OPTION_REMAINING)
            installer_info = self.get_lutris_action(url)
        except ValueError:
            self._print(command_line, _("%s is not a valid URI") % url.get_strv())
            return 1

        game_slug = installer_info["game_slug"]
        action = installer_info["action"]

        if options.contains("output-script"):
            action = "write-script"

        revision = installer_info["revision"]

        installer_file = None
        if options.contains("install"):
            installer_file = options.lookup_value("install").get_string()
            if installer_file.startswith(("http:", "https:")):
                try:
                    request = Request(installer_file).get()
                except HTTPError:
                    self._print(command_line, _("Failed to download %s") % installer_file)
                    return 1
                try:
                    headers = dict(request.response_headers)
                    file_name = headers["Content-Disposition"].split("=", 1)[-1]
                except (KeyError, IndexError):
                    file_name = os.path.basename(installer_file)
                file_path = os.path.join(tempfile.gettempdir(), file_name)
                self._print(command_line, _("download {url} to {file} started").format(
                    url=installer_file, file=file_path))
                with open(file_path, 'wb') as dest_file:
                    dest_file.write(request.content)
                installer_file = file_path
                action = "install"
            else:
                installer_file = os.path.abspath(installer_file)
                action = "install"

            if not os.path.isfile(installer_file):
                self._print(command_line, _("No such file: %s") % installer_file)
                return 1

        db_game = None
        if game_slug:
            if action == "rungameid":
                # Force db_game to use game id
                self.run_in_background = True
                db_game = games_db.get_game_by_field(game_slug, "id")
            elif action == "rungame":
                # Force db_game to use game slug
                self.run_in_background = True
                db_game = games_db.get_game_by_field(game_slug, "slug")
            elif action == "install":
                # Installers can use game or installer slugs
                self.run_in_background = True
                db_game = games_db.get_game_by_field(game_slug, "slug") \
                    or games_db.get_game_by_field(game_slug, "installer_slug")
            else:
                # Dazed and confused, try anything that might works
                db_game = (
                    games_db.get_game_by_field(game_slug, "id")
                    or games_db.get_game_by_field(game_slug, "slug")
                    or games_db.get_game_by_field(game_slug, "installer_slug")
                )

        # If reinstall flag is passed, force the action to install
        if options.contains("reinstall"):
            action = "install"

        if action == "write-script":
            if not db_game or not db_game["id"]:
                logger.warning("No game provided to generate the script")
                return 1
            self.generate_script(db_game, options.lookup_value("output-script").get_string())
            return 0

        # Graphical commands
        self.activate()
        self.set_tray_icon()

        if not action:
            if db_game and db_game["installed"]:
                # Game found but no action provided, ask what to do
                dlg = InstallOrPlayDialog(db_game["name"])
                if not dlg.action_confirmed:
                    action = None
                elif dlg.action == "play":
                    action = "rungame"
                elif dlg.action == "install":
                    action = "install"
            elif game_slug or installer_file:
                # No game found, default to install if a game_slug or
                # installer_file is provided
                action = "install"
        if action == "install":
            installers = get_installers(
                game_slug=game_slug,
                installer_file=installer_file,
                revision=revision,
            )
            if installers:
                self.show_installer_window(installers)

        elif action in ("rungame", "rungameid"):
            if not db_game or not db_game["id"]:
                logger.warning("No game found in library")
                if not self.window.is_visible():
                    self.do_shutdown()
                return 0
            game = Game(db_game["id"])
            self.on_game_launch(game)
        return 0
Ejemplo n.º 45
0
 def remove_menu_shortcut(self, *args):
     game = Game(self.view.selected_game)
     xdg.remove_launcher(game.slug, game.id, menu=True)
Ejemplo n.º 46
0
 def launch_game(self, widget, _data=None):
     """Launch a game after it's been installed."""
     widget.set_sensitive(False)
     self.on_destroy(widget)
     self.application.launch(Game(self.interpreter.installer.game_id))
Ejemplo n.º 47
0
    def do_command_line(self, command_line):
        options = command_line.get_options_dict()

        if options.contains('debug'):
            logger.setLevel(logging.DEBUG)

        if options.contains('list-games'):
            game_list = pga.get_games()
            if options.contains('installed'):
                game_list = [game for game in game_list if game['installed']]
            if options.contains('json'):
                self.print_game_json(command_line, game_list)
            else:
                self.print_game_list(command_line, game_list)
            return 0
        elif options.contains('list-steam-games'):
            self.print_steam_list(command_line)
            return 0
        elif options.contains('list-steam-folders'):
            self.print_steam_folders(command_line)
            return 0
        elif options.contains('exec'):
            command = options.lookup_value('exec').get_string()
            self.execute_command(command)
            return 0

        game_slug = ''
        revision = None
        uri = options.lookup_value(GLib.OPTION_REMAINING)
        if uri:
            uri = uri.get_strv()
        if uri and len(uri):
            uri = uri[0]  # TODO: Support multiple
            installer_info = parse_installer_url(uri)
            if installer_info is False:
                self._print(command_line, '%s is not a valid URI' % uri)
                return 1
            game_slug = installer_info['game_slug']
            revision = installer_info['revision']

        if game_slug or options.contains('install'):
            installer_file = None
            if options.contains('install'):
                installer_file = options.lookup_value('install').get_string()
                if not os.path.isfile(installer_file):
                    self._print(command_line, "No such file: %s" % installer_file)
                    return 1

            db_game = None
            if game_slug:
                db_game = (pga.get_game_by_field(game_slug, 'id') or
                           pga.get_game_by_field(game_slug, 'slug') or
                           pga.get_game_by_field(game_slug, 'installer_slug'))

            force_install = options.contains('reinstall') or bool(installer_info.get('revision'))
            if db_game and db_game['installed'] and not force_install:
                self._print(command_line, "Launching %s" % db_game['name'])
                if self.window:
                    self.run_game(db_game['id'])
                else:
                    lutris_game = Game(db_game['id'])
                    # FIXME: This is awful
                    lutris_game.exit_main_loop = True
                    lutris_game.play()
                    try:
                        GLib.MainLoop().run()
                    except KeyboardInterrupt:
                        lutris_game.stop()
            else:
                self._print(command_line, "Installing %s" % game_slug or installer_file)
                if self.window:
                    self.window.on_install_clicked(game_slug=game_slug,
                                                   installer_file=installer_file,
                                                   revision=revision)
                else:
                    runtime_updater = RuntimeUpdater()
                    runtime_updater.update()
                    # FIXME: This should be a Gtk.Dialog child of LutrisWindow
                    dialog = InstallerDialog(game_slug=game_slug,
                                             installer_file=installer_file,
                                             revision=revision)
                    self.add_window(dialog)
            return 0

        self.activate()
        return 0
Ejemplo n.º 48
0
    def do_command_line(self, command_line):
        options = command_line.get_options_dict()

        # Use stdout to output logs, only if no command line argument is
        # provided
        argc = len(sys.argv) - 1
        if "-d" in sys.argv or "--debug" in sys.argv:
            argc -= 1
        if not argc:
            # Switch back the log output to stderr (the default in Python)
            # to avoid messing with any output from command line options.

            # Use when targetting Python 3.7 minimum
            # console_handler.setStream(sys.stderr)

            # Until then...
            logger.removeHandler(log.console_handler)
            log.console_handler = logging.StreamHandler(stream=sys.stdout)
            log.console_handler.setFormatter(log.SIMPLE_FORMATTER)
            logger.addHandler(log.console_handler)

        # Set up logger
        if options.contains("debug"):
            log.console_handler.setFormatter(log.DEBUG_FORMATTER)
            logger.setLevel(logging.DEBUG)

        # Text only commands

        # Print Lutris version and exit
        if options.contains("version"):
            executable_name = os.path.basename(sys.argv[0])
            print(executable_name + "-" + settings.VERSION)
            logger.setLevel(logging.NOTSET)
            return 0

        logger.info("Running Lutris %s", settings.VERSION)
        migrate()
        run_all_checks()
        AsyncCall(init_dxvk_versions)

        # List game
        if options.contains("list-games"):
            game_list = pga.get_games()
            if options.contains("installed"):
                game_list = [game for game in game_list if game["installed"]]
            if options.contains("json"):
                self.print_game_json(command_line, game_list)
            else:
                self.print_game_list(command_line, game_list)
            return 0
        # List Steam games
        elif options.contains("list-steam-games"):
            self.print_steam_list(command_line)
            return 0
        # List Steam folders
        elif options.contains("list-steam-folders"):
            self.print_steam_folders(command_line)
            return 0

        # Execute command in Lutris context
        elif options.contains("exec"):
            command = options.lookup_value("exec").get_string()
            self.execute_command(command)
            return 0

        try:
            url = options.lookup_value(GLib.OPTION_REMAINING)
            installer_info = self.get_lutris_action(url)
        except ValueError:
            self._print(command_line, "%s is not a valid URI" % url.get_strv())
            return 1
        game_slug = installer_info["game_slug"]
        action = installer_info["action"]
        revision = installer_info["revision"]

        installer_file = None
        if options.contains("install"):
            installer_file = options.lookup_value("install").get_string()
            installer_file = os.path.abspath(installer_file)
            action = "install"
            if not os.path.isfile(installer_file):
                self._print(command_line, "No such file: %s" % installer_file)
                return 1

        # Graphical commands
        self.activate()
        self.set_tray_icon()

        db_game = None
        if game_slug:
            if action == "rungameid":
                # Force db_game to use game id
                db_game = pga.get_game_by_field(game_slug, "id")
            elif action == "rungame":
                # Force db_game to use game slug
                db_game = pga.get_game_by_field(game_slug, "slug")
            elif action == "install":
                # Installers can use game or installer slugs
                db_game = pga.get_game_by_field(
                    game_slug, "slug") or pga.get_game_by_field(
                        game_slug, "installer_slug")

            else:
                # Dazed and confused, try anything that might works
                db_game = (pga.get_game_by_field(game_slug, "id")
                           or pga.get_game_by_field(game_slug, "slug") or
                           pga.get_game_by_field(game_slug, "installer_slug"))

        if not action:
            if db_game and db_game["installed"]:
                # Game found but no action provided, ask what to do
                dlg = InstallOrPlayDialog(db_game["name"])
                if not dlg.action_confirmed:
                    action = None
                elif dlg.action == "play":
                    action = "rungame"
                elif dlg.action == "install":
                    action = "install"
            elif game_slug or installer_file:
                # No game found, default to install if a game_slug or
                # installer_file is provided
                action = "install"

        if action == "install":
            InstallerWindow(
                game_slug=game_slug,
                installer_file=installer_file,
                revision=revision,
                parent=self.window,
                application=self,
            )
        elif action in ("rungame", "rungameid"):
            if not db_game or not db_game["id"]:
                logger.warning("No game found in library")
                return 0

            logger.info("Launching %s", db_game["name"])

            # If game is not installed, show the GUI before running. Otherwise leave the GUI closed.
            if not db_game["installed"]:
                self.window.present()
            self.launch(Game(db_game["id"]))

        else:
            self.window.present()

        return 0
Ejemplo n.º 49
0
 def run(self):
     egs = get_game_by_field(self.client_installer, "slug")
     egs_game = Game(egs["id"])
     egs_game.emit("game-launch")
Ejemplo n.º 50
0
class LutrisWindow(Gtk.Application):
    """Handler class for main window signals."""
    def __init__(self, service=None):

        Gtk.Application.__init__(
            self,
            application_id="net.lutris.main",
            flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
        ui_filename = os.path.join(datapath.get(), 'ui', 'lutris-window.ui')
        if not os.path.exists(ui_filename):
            raise IOError('File %s not found' % ui_filename)

        self.service = service
        self.runtime_updater = RuntimeUpdater()
        self.running_game = None
        self.threads_stoppers = []

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

        self.builder = Gtk.Builder()
        self.builder.add_from_file(ui_filename)

        # Load settings
        width = int(settings.read_setting('width') or 800)
        height = int(settings.read_setting('height') or 600)
        self.window_size = (width, height)
        window = self.builder.get_object('window')
        window.resize(width, height)
        view_type = self.get_view_type()
        self.load_icon_type_from_settings(view_type)
        self.filter_installed = \
            settings.read_setting('filter_installed') == 'true'
        self.sidebar_visible = \
            settings.read_setting('sidebar_visible') in ['true', None]

        # Set theme to dark if set in the settings
        dark_theme_menuitem = self.builder.get_object('dark_theme_menuitem')
        use_dark_theme = settings.read_setting('dark_theme') == 'true'
        dark_theme_menuitem.set_active(use_dark_theme)
        self.set_dark_theme(use_dark_theme)

        self.game_list = pga.get_games()

        # Load view
        self.game_store = GameStore([], self.icon_type, self.filter_installed)
        self.view = self.get_view(view_type)

        self.main_box = self.builder.get_object('main_box')
        self.splash_box = self.builder.get_object('splash_box')
        self.connect_link = self.builder.get_object('connect_link')
        # View menu
        installed_games_only_menuitem =\
            self.builder.get_object('filter_installed')
        installed_games_only_menuitem.set_active(self.filter_installed)
        self.grid_view_menuitem = self.builder.get_object("gridview_menuitem")
        self.grid_view_menuitem.set_active(view_type == 'grid')
        self.list_view_menuitem = self.builder.get_object("listview_menuitem")
        self.list_view_menuitem.set_active(view_type == 'list')
        sidebar_menuitem = self.builder.get_object('sidebar_menuitem')
        sidebar_menuitem.set_active(self.sidebar_visible)

        # View buttons
        self.grid_view_btn = self.builder.get_object('switch_grid_view_btn')
        self.grid_view_btn.set_active(view_type == 'grid')
        self.list_view_btn = self.builder.get_object('switch_list_view_btn')
        self.list_view_btn.set_active(view_type == 'list')
        # Icon type menu
        self.banner_small_menuitem = \
            self.builder.get_object('banner_small_menuitem')
        self.banner_small_menuitem.set_active(self.icon_type == 'banner_small')
        self.banner_menuitem = self.builder.get_object('banner_menuitem')
        self.banner_menuitem.set_active(self.icon_type == 'banner')
        self.icon_menuitem = self.builder.get_object('icon_menuitem')
        self.icon_menuitem.set_active(self.icon_type == 'icon')

        self.search_entry = self.builder.get_object('search_entry')
        self.search_entry.connect('icon-press', self.on_clear_search)

        # Scroll window
        self.games_scrollwindow = self.builder.get_object('games_scrollwindow')
        self.games_scrollwindow.add(self.view)
        # Buttons
        self.stop_button = self.builder.get_object('stop_button')
        self.stop_button.set_sensitive(False)
        self.delete_button = self.builder.get_object('delete_button')
        self.delete_button.set_sensitive(False)
        self.play_button = self.builder.get_object('play_button')
        self.play_button.set_sensitive(False)

        # Contextual menu
        main_entries = [
            ('play', "Play", self.on_game_run),
            ('install', "Install", self.on_install_clicked),
            ('add', "Add manually", self.on_add_manually),
            ('configure', "Configure", self.on_edit_game_configuration),
            ('browse', "Browse files", self.on_browse_files),
            ('desktop-shortcut', "Create desktop shortcut",
             self.create_desktop_shortcut),
            ('rm-desktop-shortcut', "Delete desktop shortcut",
             self.remove_desktop_shortcut),
            ('menu-shortcut', "Create application menu shortcut",
             self.create_menu_shortcut),
            ('rm-menu-shortcut', "Delete application menu shortcut",
             self.remove_menu_shortcut),
            ('install_more', "Install (add) another version",
             self.on_install_clicked),
            ('remove', "Remove", self.on_remove_game),
        ]
        self.menu = ContextualMenu(main_entries)
        self.view.contextual_menu = self.menu

        # Sidebar
        self.sidebar_paned = self.builder.get_object('sidebar_paned')
        self.sidebar_treeview = SidebarTreeView()
        self.sidebar_treeview.connect('cursor-changed',
                                      self.on_sidebar_changed)
        self.sidebar_viewport = self.builder.get_object('sidebar_viewport')
        self.sidebar_viewport.add(self.sidebar_treeview)

        # Window initialization
        self.window = self.builder.get_object("window")
        self.window.resize_to_geometry(width, height)
        self.window.set_default_icon_name('lutris')
        self.window.show_all()
        self.builder.connect_signals(self)
        self.connect_signals()

        self.statusbar = self.builder.get_object("statusbar")

        # XXX Hide PGA config menu item until it actually gets implemented
        pga_menuitem = self.builder.get_object('pga_menuitem')
        pga_menuitem.hide()

        # Sync local lutris library with current Steam games before setting up
        # view
        steam.sync_with_lutris()

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

        self.show_sidebar()
        self.update_runtime()

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

        # Timers
        self.timer_ids = [GLib.timeout_add(300, self.refresh_status)]
        steamapps_paths = steam.get_steamapps_paths(flat=True)
        self.steam_watcher = steam.SteamWatcher(steamapps_paths,
                                                self.on_steam_game_changed)

    @property
    def current_view_type(self):
        return 'grid' \
            if self.view.__class__.__name__ == "GameFlowBox" \
            else 'list'

    def on_steam_game_changed(self, operation, path):
        appmanifest = steam.AppManifest(path)
        runner_name = appmanifest.get_runner_name()
        games = pga.get_game_by_field(appmanifest.steamid,
                                      field='steamid',
                                      all=True)
        if operation == 'DELETE':
            for game in games:
                if game['runner'] == runner_name:
                    steam.mark_as_uninstalled(game)
                    self.view.set_uninstalled(Game(game['id']))
                    break
        elif operation in ('MODIFY', 'CREATE'):
            if not appmanifest.is_installed():
                return
            if runner_name == 'windows':
                return
            game_info = None
            for game in games:
                if game['installed'] == 0:
                    game_info = game
            if not game_info:
                game_info = {
                    'name': appmanifest.name,
                    'slug': appmanifest.slug,
                }
            game_id = steam.mark_as_installed(appmanifest.steamid, runner_name,
                                              game_info)
            game_ids = [game['id'] for game in self.game_list]
            if game_id not in game_ids:
                self.add_game_to_view(game_id)
            else:
                self.view.set_installed(Game(game_id))

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

    def get_view(self, view_type):
        if view_type == 'grid' and flowbox.FLOWBOX_SUPPORTED:
            return flowbox.GameFlowBox(self.game_list,
                                       icon_type=self.icon_type,
                                       filter_installed=self.filter_installed)
        else:
            return GameListView(self.game_store)

    def connect_signals(self):
        """Connect signals from the view with the main window.

        This must be called each time the view is rebuilt.
        """
        self.view.connect('game-installed', self.on_game_installed)
        self.view.connect("game-activated", self.on_game_run)
        self.view.connect("game-selected", self.game_selection_changed)
        self.window.connect("configure-event", self.on_resize)

    def check_update(self):
        """Verify availability of client update."""
        version_request = http.Request('https://lutris.net/version')
        version_request.get()
        version = version_request.content
        if version:
            latest_version = settings.read_setting('latest_version')
            if version > (latest_version or settings.VERSION):
                dialogs.ClientUpdateDialog()
                # Store latest version seen to avoid showing
                # the dialog more than once.
                settings.write_setting('latest_version', version)

    def get_view_type(self):
        if not flowbox.FLOWBOX_SUPPORTED:
            return 'list'
        view_type = settings.read_setting('view_type')
        if view_type in ['grid', 'list']:
            return view_type
        return settings.GAME_VIEW

    def load_icon_type_from_settings(self, view_type):
        """Return the icon style depending on the type of view."""
        if view_type == 'list':
            self.icon_type = settings.read_setting('icon_type_listview')
            default = settings.ICON_TYPE_LISTVIEW
        else:
            self.icon_type = settings.read_setting('icon_type_gridview')
            default = settings.ICON_TYPE_GRIDVIEW
        if self.icon_type not in ("banner_small", "banner", "icon"):
            self.icon_type = default
        return self.icon_type

    def switch_splash_screen(self):
        if len(self.game_list) == 0:
            self.splash_box.show()
            self.sidebar_paned.hide()
            self.games_scrollwindow.hide()
        else:
            self.splash_box.hide()
            self.sidebar_paned.show()
            self.games_scrollwindow.show()

    def switch_view(self, view_type):
        """Switch between grid view and list view."""
        self.view.destroy()
        self.load_icon_type_from_settings(view_type)
        self.game_store.set_icon_type(self.icon_type)

        self.view = self.get_view(view_type)
        self.view.contextual_menu = self.menu
        self.connect_signals()
        scrollwindow_children = self.games_scrollwindow.get_children()
        if len(scrollwindow_children):
            child = scrollwindow_children[0]
            child.destroy()
        self.games_scrollwindow.add(self.view)
        self.view.show_all()

        # Note: set_active(True *or* False) apparently makes ALL the menuitems
        # in the group send the activate signal...
        if self.icon_type == 'banner_small':
            self.banner_small_menuitem.set_active(True)
        elif self.icon_type == 'icon':
            self.icon_menuitem.set_active(True)
        elif self.icon_type == 'banner':
            self.banner_menuitem.set_active(True)
        settings.write_setting('view_type', view_type)

    def sync_library(self):
        """Synchronize games with local stuff and server."""
        def update_gui(result, error):
            if result:
                added_ids, updated_ids = result
                added_games = pga.get_game_by_field(added_ids, 'id', all=True)
                self.game_list += added_games
                self.view.populate_games(added_games)
                self.switch_splash_screen()
                GLib.idle_add(self.update_existing_games, added_ids,
                              updated_ids, True)
            else:
                logger.error("No results returned when syncing the library")

        self.set_status("Syncing library")
        AsyncCall(sync_from_remote, update_gui)

    def update_existing_games(self, added, updated, first_run=False):
        for game_id in updated.difference(added):
            self.view.update_row(pga.get_game_by_field(game_id, 'id'))

        if first_run:
            icons_sync = AsyncCall(self.sync_icons, callback=None)
            self.threads_stoppers.append(icons_sync.stop_request.set)
            self.set_status("")

    def update_runtime(self):
        self.runtime_updater.update(self.set_status)
        self.threads_stoppers += self.runtime_updater.cancellables

    def sync_icons(self):
        resources.fetch_icons([game['slug'] for game in self.game_list],
                              callback=self.on_image_downloaded)

    def set_status(self, text):
        status_box = self.builder.get_object('status_box')
        for child_widget in status_box.get_children():
            child_widget.destroy()
        label = Gtk.Label(text)
        label.show()
        status_box.add(label)

    def refresh_status(self):
        """Refresh status bar."""
        if self.running_game:
            name = self.running_game.name
            if self.running_game.state == self.running_game.STATE_IDLE:
                self.set_status("Preparing to launch %s" % name)
            elif self.running_game.state == self.running_game.STATE_STOPPED:
                self.set_status("Game has quit")
                self.stop_button.set_sensitive(False)
            elif self.running_game.state == self.running_game.STATE_RUNNING:
                self.set_status("Playing %s" % name)
                self.stop_button.set_sensitive(True)
        return True

    # ---------
    # Callbacks
    # ---------

    def on_dark_theme_toggled(self, widget):
        use_dark_theme = widget.get_active()
        setting_value = 'true' if use_dark_theme else 'false'
        settings.write_setting('dark_theme', setting_value)
        self.set_dark_theme(use_dark_theme)

    def on_clear_search(self, widget, icon_pos, event):
        if icon_pos == Gtk.EntryIconPosition.SECONDARY:
            widget.set_text('')

    def on_connect(self, *args):
        """Callback when a user connects to his account."""
        login_dialog = dialogs.ClientLoginDialog(self.window)
        login_dialog.connect('connected', self.on_connect_success)
        return True

    def on_connect_success(self, dialog, credentials):
        if isinstance(credentials, str):
            username = credentials
        else:
            username = credentials["username"]
        self.toggle_connection(True, username)
        self.sync_library()
        self.connect_link.hide()
        synchronize_menuitem = self.builder.get_object('synchronize_menuitem')
        synchronize_menuitem.set_sensitive(True)

    def on_disconnect(self, *args):
        api.disconnect()
        self.toggle_connection(False)
        self.connect_link.show()

        synchronize_menuitem = self.builder.get_object('synchronize_menuitem')
        synchronize_menuitem.set_sensitive(False)

    def toggle_connection(self, is_connected, username=None):
        disconnect_menuitem = self.builder.get_object('disconnect_menuitem')
        connect_menuitem = self.builder.get_object('connect_menuitem')
        connection_label = self.builder.get_object('connection_label')

        if is_connected:
            disconnect_menuitem.show()
            connect_menuitem.hide()
            connection_status = username
            logger.info('Connected to lutris.net as %s', connection_status)
        else:
            disconnect_menuitem.hide()
            connect_menuitem.show()
            connection_status = "Not connected"
        connection_label.set_text(connection_status)

    def on_games_button_clicked(self, widget):
        self._open_browser("https://lutris.net/games/")

    def on_register_account(self, *args):
        self._open_browser("https://lutris.net/user/register")

    def _open_browser(self, url):
        try:
            subprocess.check_call(["xdg-open", url])
        except subprocess.CalledProcessError:
            Gtk.show_uri(None, url, Gdk.CURRENT_TIME)

    def on_synchronize_manually(self, widget):
        """Callback when Synchronize Library is activated."""
        self.sync_library()

    def on_resize(self, widget, *args):
        """WTF is this doing?"""
        self.window_size = widget.get_size()

    def on_destroy(self, *args):
        """Signal for window close."""
        # Stop cancellable running threads
        for stopper in self.threads_stoppers:
            stopper()
        self.steam_watcher.stop()

        if self.running_game \
           and self.running_game.state != self.running_game.STATE_STOPPED:
            logger.info("%s is still running, stopping it",
                        self.running_game.name)
            self.running_game.stop()

        if self.service:
            self.service.stop()

        # Save settings
        width, height = self.window_size
        settings.write_setting('width', width)
        settings.write_setting('height', height)

        Gtk.main_quit(*args)

    def on_runners_activate(self, _widget, _data=None):
        """Callback when manage runners is activated."""
        RunnersDialog()

    def on_preferences_activate(self, _widget, _data=None):
        """Callback when preferences is activated."""
        SystemConfigDialog(parent=self.window)

    def on_show_installed_games_toggled(self, widget, data=None):
        filter_installed = widget.get_active()
        setting_value = 'true' if filter_installed else 'false'
        settings.write_setting('filter_installed', setting_value)
        if self.current_view_type == 'grid':
            self.view.filter_installed = filter_installed
            self.view.invalidate_filter()
        else:
            self.game_store.filter_installed = filter_installed
            self.game_store.modelfilter.refilter()

    def on_pga_menuitem_activate(self, _widget, _data=None):
        dialogs.PgaSourceDialog(parent=self.window)

    def on_search_entry_changed(self, widget):
        if self.current_view_type == 'grid':
            self.view.filter_text = widget.get_text()
            self.view.invalidate_filter()
        else:
            self.game_store.filter_text = widget.get_text()
            self.game_store.modelfilter.refilter()

    def on_about_clicked(self, _widget, _data=None):
        """Open the about dialog."""
        dialogs.AboutDialog(parent=self.window)

    def _get_current_game_id(self):
        """Return the id of the current selected game while taking care of the
        double clic bug.
        """
        # Wait two seconds to avoid running a game twice
        if time.time() - self.game_launch_time < 2:
            return
        self.game_launch_time = time.time()
        return self.view.selected_game

    def on_game_run(self, _widget=None, game_id=None):
        """Launch a game, or install it if it is not"""
        if not game_id:
            game_id = self._get_current_game_id()
        if not game_id:
            return
        self.running_game = Game(game_id)
        if self.running_game.is_installed:
            self.running_game.play()
        else:
            game_slug = self.running_game.slug
            self.running_game = None
            InstallerDialog(game_slug, self)

    def on_game_stop(self, *args):
        """Stop running game."""
        if self.running_game:
            self.running_game.stop()
            self.stop_button.set_sensitive(False)

    def on_install_clicked(self, _widget=None, game_ref=None):
        """Install a game"""
        if not game_ref:
            game_id = self._get_current_game_id()
            game = pga.get_game_by_field(game_id, 'id')
            game_ref = game.get('slug')
            logger.debug("Installing game %s (%s)" % (game_ref, game_id))
        if not game_ref:
            return
        InstallerDialog(game_ref, self)

    def game_selection_changed(self, _widget):
        # Emulate double click to workaround GTK bug #484640
        # https://bugzilla.gnome.org/show_bug.cgi?id=484640
        if type(self.view) is GameGridView:
            is_double_click = time.time() - self.game_selection_time < 0.4
            is_same_game = self.view.selected_game == self.last_selected_game
            if is_double_click and is_same_game:
                self.on_game_run()
            self.game_selection_time = time.time()
            self.last_selected_game = self.view.selected_game

        sensitive = True if self.view.selected_game else False
        self.play_button.set_sensitive(sensitive)
        self.delete_button.set_sensitive(sensitive)

    def on_game_installed(self, view, game_id):
        if type(game_id) != int:
            raise ValueError("game_id must be an int")
        if not self.view.has_game_id(game_id):
            logger.debug("Adding new installed game to view (%d)" % game_id)
            self.add_game_to_view(game_id, async=False)

        game = Game(game_id)
        view.set_installed(game)
        self.sidebar_treeview.update()
        GLib.idle_add(resources.fetch_icons, [game.slug],
                      self.on_image_downloaded)

    def on_image_downloaded(self, game_slugs):
        for game_slug in game_slugs:
            games = pga.get_game_by_field(game_slug, field='slug', all=True)
            for game in games:
                game = Game(game['id'])
                is_installed = game.is_installed
                self.view.update_image(game.id, is_installed)

    def on_add_manually(self, widget, *args):
        def on_game_added(game):
            self.view.set_installed(game)
            self.sidebar_treeview.update()

        game = Game(self.view.selected_game)
        AddGameDialog(self.window,
                      game=game,
                      runner=self.selected_runner,
                      callback=lambda: on_game_added(game))

    def on_view_game_log_activate(self, widget):
        if not self.running_game:
            dialogs.ErrorDialog('No game log available')
            return
        log_title = u"Log for {}".format(self.running_game)
        log_window = LogWindow(log_title, self.window)
        log_window.logtextview.set_text(self.running_game.game_log)
        log_window.run()
        log_window.destroy()

    def on_add_game_button_clicked(self, _widget, _data=None):
        """Add a new game manually with the AddGameDialog."""
        dialog = AddGameDialog(
            self.window,
            runner=self.selected_runner,
            callback=lambda: self.add_game_to_view(dialog.game.id))
        return True

    def add_game_to_view(self, game_id, async=True):
        if not game_id:
            raise ValueError("Missing game id")

        def do_add_game():
            self.view.add_game_by_id(game_id)
            self.switch_splash_screen()
            self.sidebar_treeview.update()

        if async:
            GLib.idle_add(do_add_game)
        else:
            do_add_game()
Ejemplo n.º 51
0
 def on_game_installed(self, view, slug):
     view.set_installed(Game(slug))
Ejemplo n.º 52
0
 def launch_game(self, widget, _data=None):
     """Launch a game after it's been installed."""
     widget.set_sensitive(False)
     self.on_destroy(widget)
     game = Game(self.interpreter.installer.game_id)
     game.emit("game-launch")
Ejemplo n.º 53
0
class UninstallGameDialog(GtkBuilderDialog):
    glade_file = 'dialog-uninstall-game.ui'
    dialog_object = 'uninstall-game-dialog'

    def substitute_label(self, widget, name, replacement):
        if hasattr(widget, 'get_text'):
            get_text = widget.get_text
            set_text = widget.set_text
        elif hasattr(widget, 'get_label'):
            get_text = widget.get_label
            set_text = widget.set_label
        else:
            raise TypeError("Unsupported type %s" % type(widget))

        replacement = replacement.replace('&', '&amp;')
        set_text(get_text().replace("{%s}" % name, replacement))

    def initialize(self, slug=None, callback=None):
        self.game = Game(slug)
        self.callback = callback
        runner = self.game.runner

        self.substitute_label(self.builder.get_object('description_label'),
                              'game', self.game.name)

        self.substitute_label(
            self.builder.get_object('remove_from_library_button'),
            'game', self.game.name
        )
        remove_contents_button = self.builder.get_object(
            'remove_contents_button'
        )
        if self.game.is_installed:
            if hasattr(runner, 'own_game_remove_method'):
                remove_contents_button.set_label(runner.own_game_remove_method)
            else:
                try:
                    default_path = runner.default_path
                except AttributeError:
                    default_path = "/"
                if not is_removeable(runner.game_path, excludes=[default_path]):
                    remove_contents_button.set_sensitive(False)

            path = self.game.directory or 'disk'
            self.substitute_label(remove_contents_button, 'path', path)
            remove_contents_button.get_children()[0].set_use_markup(True)
        else:
            remove_contents_button.hide()

        cancel_button = self.builder.get_object('cancel_button')
        cancel_button.connect('clicked', self.on_close)

        apply_button = self.builder.get_object('apply_button')
        apply_button.connect('clicked', self.on_apply_button_clicked)

    def on_apply_button_clicked(self, widget):
        widget.set_sensitive(False)

        remove_from_library_button = self.builder.get_object(
            'remove_from_library_button'
        )
        remove_from_library = remove_from_library_button.get_active()
        remove_contents_button = self.builder.get_object(
            'remove_contents_button'
        )
        remove_contents = remove_contents_button.get_active()
        if remove_contents and not hasattr(self.game.runner,
                                           'no_game_remove_warning'):
            game_dir = self.game.directory.replace('&', '&amp;')
            dlg = QuestionDialog({
                'question': "Are you sure you want to delete EVERYTHING under "
                            "\n<b>%s</b>?\n (This can't be undone)"
                            % game_dir,
                'title': "CONFIRM DANGEROUS OPERATION"
            })
            if dlg.result != Gtk.ResponseType.YES:
                widget.set_sensitive(True)
                return

        self.game.remove(remove_from_library, remove_contents)
        self.callback(self.game.slug, remove_from_library)

        self.on_close()
Ejemplo n.º 54
0
class GameActions:
    """Regroup a list of callbacks for a game"""
    def __init__(self, application=None, window=None):
        self.application = application or Gio.Application.get_default()
        self.window = window
        self.game_id = None
        self._game = None

    @property
    def game(self):
        if not self._game:
            self._game = self.application.get_game_by_id(self.game_id)
            if not self._game:
                self._game = Game(self.game_id)
            self._game.connect("game-error", self.window.on_game_error)
        return self._game

    @property
    def is_game_running(self):
        return bool(self.application.get_game_by_id(self.game_id))

    def set_game(self, game=None, game_id=None):
        if game:
            self._game = game
            self.game_id = game.id
        else:
            self._game = None
            self.game_id = game_id

    def get_game_actions(self):
        """Return a list of game actions and their callbacks"""
        return [
            ("play", "Play", self.on_game_run),
            ("stop", "Stop", self.on_stop),
            ("show_logs", "Show logs", self.on_show_logs),
            ("install", "Install", self.on_install_clicked),
            ("add", "Add installed game", self.on_add_manually),
            ("configure", "Configure", self.on_edit_game_configuration),
            ("execute-script", "Execute script",
             self.on_execute_script_clicked),
            ("browse", "Browse files", self.on_browse_files),
            (
                "desktop-shortcut",
                "Create desktop shortcut",
                self.on_create_desktop_shortcut,
            ),
            (
                "rm-desktop-shortcut",
                "Delete desktop shortcut",
                self.on_remove_desktop_shortcut,
            ),
            (
                "menu-shortcut",
                "Create application menu shortcut",
                self.on_create_menu_shortcut,
            ),
            (
                "rm-menu-shortcut",
                "Delete application menu shortcut",
                self.on_remove_menu_shortcut,
            ),
            ("install_more", "Install another version",
             self.on_install_clicked),
            ("remove", "Remove", self.on_remove_game),
            ("view", "View on Lutris.net", self.on_view_game),
        ]

    def get_displayed_entries(self):
        """Return a dictionary of actions that should be shown for a game"""
        return {
            "add":
            not self.game.is_installed and not self.game.is_search_result,
            "install":
            not self.game.is_installed,
            "play":
            self.game.is_installed and not self.is_game_running,
            "stop":
            self.is_game_running,
            "show_logs":
            self.game.is_installed,
            "configure":
            bool(self.game.is_installed),
            "install_more":
            self.game.is_installed and not self.game.is_search_result,
            "execute-script":
            bool(self.game.is_installed
                 and self.game.runner.system_config.get("manual_command")),
            "desktop-shortcut": (self.game.is_installed
                                 and not xdgshortcuts.desktop_launcher_exists(
                                     self.game.slug, self.game.id)),
            "menu-shortcut": (self.game.is_installed
                              and not xdgshortcuts.menu_launcher_exists(
                                  self.game.slug, self.game.id)),
            "rm-desktop-shortcut":
            bool(self.game.is_installed
                 and xdgshortcuts.desktop_launcher_exists(
                     self.game.slug, self.game.id)),
            "rm-menu-shortcut":
            bool(self.game.is_installed and xdgshortcuts.menu_launcher_exists(
                self.game.slug, self.game.id)),
            "browse":
            self.game.is_installed and self.game.runner_name != "browser",
            "remove":
            not self.game.is_search_result,
            "view":
            True
        }

    def get_disabled_entries(self):
        """Return a dictionary of actions that should be disabled for a game"""
        return {
            "show_logs": not bool(self.game.game_thread),
        }

    def on_game_run(self, *_args):
        """Launch a game"""
        self.application.launch(self.game)

    def get_running_game(self):
        for i in range(self.application.running_games.get_n_items()):
            game = self.application.running_games.get_item(i)
            if game == self.game:
                return game

    def on_stop(self, caller):
        """Stops the game"""

        matched_game = self.get_running_game()
        if not matched_game:
            logger.warning("%s not in running game list", self.game_id)
            return

        try:
            os.kill(matched_game.game_thread.game_process.pid, signal.SIGTERM)
        except ProcessLookupError:
            pass
        logger.debug("Removed game with ID %s from running games",
                     self.game_id)

    def on_show_logs(self, _widget):
        """Display game log"""
        return LogWindow(title="Log for {}".format(self.game),
                         buffer=self.game.log_buffer,
                         application=self.application)

    def on_install_clicked(self, *_args):
        """Install a game"""
        # Install the currently selected game in the UI
        return InstallerWindow(
            parent=self.window,
            game_slug=self.game.slug,
            application=self.application,
        )

    def on_add_manually(self, _widget, *_args):
        """Callback that presents the Add game dialog"""
        AddGameDialog(self.window,
                      game=self.game,
                      runner=self.game.runner_name)

    def on_edit_game_configuration(self, _widget):
        """Edit game preferences"""
        EditGameConfigDialog(self.window, self.game)

    def on_execute_script_clicked(self, _widget):
        """Execute the game's associated script"""
        manual_command = self.game.runner.system_config.get("manual_command")
        if path_exists(manual_command):
            MonitoredCommand(
                [manual_command],
                include_processes=[os.path.basename(manual_command)],
                cwd=self.game.directory,
            ).start()
            logger.info("Running %s in the background", manual_command)

    def on_browse_files(self, _widget):
        """Callback to open a game folder in the file browser"""
        path = self.game.get_browse_dir()
        if not path:
            dialogs.NoticeDialog("This game has no installation directory")
        elif path_exists(path):
            if self.game.runner.system_config.get("use_xdg_utils"):
                subprocess.run(["xdg-open", path])
            else:
                open_uri("file://%s" % path)
        else:
            dialogs.NoticeDialog("Can't open %s \nThe folder doesn't exist." %
                                 path)

    def on_create_menu_shortcut(self, *_args):
        """Add the selected game to the system's Games menu."""
        xdgshortcuts.create_launcher(self.game.slug,
                                     self.game.id,
                                     self.game.name,
                                     menu=True)

    def on_create_desktop_shortcut(self, *_args):
        """Create a desktop launcher for the selected game."""
        xdgshortcuts.create_launcher(self.game.slug,
                                     self.game.id,
                                     self.game.name,
                                     desktop=True)

    def on_remove_menu_shortcut(self, *_args):
        """Remove an XDG menu shortcut"""
        xdgshortcuts.remove_launcher(self.game.slug, self.game.id, menu=True)

    def on_remove_desktop_shortcut(self, *_args):
        """Remove a .desktop shortcut"""
        xdgshortcuts.remove_launcher(self.game.slug,
                                     self.game.id,
                                     desktop=True)

    def on_view_game(self, _widget):
        """Callback to open a game on lutris.net"""
        open_uri("https://lutris.net/games/%s" % self.game.slug)

    def on_remove_game(self, *_args):
        """Callback that present the uninstall dialog to the user"""
        UninstallGameDialog(game_id=self.game.id,
                            callback=self.window.remove_game_from_view,
                            parent=self.window)
Ejemplo n.º 55
0
class GameActions:
    """Regroup a list of callbacks for a game"""
    def __init__(self, application=None, window=None):
        self.application = application or Gio.Application.get_default()
        self.window = window
        self.game_id = None
        self._game = None

    @property
    def game(self):
        if not self._game:
            self._game = self.application.get_game_by_id(self.game_id)
            if not self._game:
                self._game = Game(self.game_id)
            self._game.connect("game-error", self.window.on_game_error)
        return self._game

    @property
    def is_game_running(self):
        return bool(self.application.get_game_by_id(self.game_id))

    def set_game(self, game=None, game_id=None):
        if game:
            self._game = game
            self.game_id = game.id
        else:
            self._game = None
            self.game_id = game_id

    def get_game_actions(self):
        """Return a list of game actions and their callbacks"""
        return [
            (
                "play", "Play",
                self.on_game_run
            ),
            (
                "stop", "Stop",
                self.on_stop
            ),
            (
                "show_logs", "Show logs",
                self.on_show_logs
            ),
            (
                "install", "Install",
                self.on_install_clicked
            ),
            (
                "add", "Add installed game",
                self.on_add_manually
            ),
            (
                "configure", "Configure",
                self.on_edit_game_configuration
            ),
            (
                "execute-script", "Execute script",
                self.on_execute_script_clicked
            ),
            (
                "browse", "Browse files",
                self.on_browse_files
            ),
            (
                "desktop-shortcut", "Create desktop shortcut",
                self.on_create_desktop_shortcut,
            ),
            (
                "rm-desktop-shortcut", "Delete desktop shortcut",
                self.on_remove_desktop_shortcut,
            ),
            (
                "menu-shortcut", "Create application menu shortcut",
                self.on_create_menu_shortcut,
            ),
            (
                "rm-menu-shortcut", "Delete application menu shortcut",
                self.on_remove_menu_shortcut,
            ),
            (
                "install_more", "Install another version",
                self.on_install_clicked
            ),
            (
                "remove", "Remove",
                self.on_remove_game
            ),
            (
                "view", "View on Lutris.net",
                self.on_view_game
            ),
        ]

    def get_displayed_entries(self):
        """Return a dictionary of actions that should be shown for a game"""
        return {
            "add": not self.game.is_installed and not self.game.is_search_result,
            "install": not self.game.is_installed,
            "play": self.game.is_installed and not self.is_game_running,
            "stop": self.is_game_running,
            "show_logs": self.game.is_installed,
            "configure": bool(self.game.is_installed),
            "install_more": self.game.is_installed and not self.game.is_search_result,
            "execute-script": bool(
                self.game.is_installed
                and self.game.runner.system_config.get("manual_command")
            ),
            "desktop-shortcut": (
                self.game.is_installed
                and not xdgshortcuts.desktop_launcher_exists(self.game.slug, self.game.id)
            ),
            "menu-shortcut": (
                self.game.is_installed
                and not xdgshortcuts.menu_launcher_exists(self.game.slug, self.game.id)
            ),
            "rm-desktop-shortcut": bool(
                self.game.is_installed
                and xdgshortcuts.desktop_launcher_exists(self.game.slug, self.game.id)
            ),
            "rm-menu-shortcut": bool(
                self.game.is_installed
                and xdgshortcuts.menu_launcher_exists(self.game.slug, self.game.id)
            ),
            "browse": self.game.is_installed and self.game.runner_name != "browser",
            "remove": not self.game.is_search_result,
            "view": True
        }

    def on_game_run(self, *_args):
        """Launch a game"""
        self.application.launch(self.game)

    def get_running_game(self):
        for i in range(self.application.running_games.get_n_items()):
            game = self.application.running_games.get_item(i)
            if game == self.game:
                return game

    def on_stop(self, caller):
        """Stops the game"""

        matched_game = self.get_running_game()
        if not matched_game:
            logger.warning("%s not in running game list", self.game_id)
            return

        try:
            os.kill(matched_game.game_thread.game_process.pid, signal.SIGTERM)
        except ProcessLookupError:
            pass
        logger.debug("Removed game with ID %s from running games", self.game_id)

    def on_show_logs(self, _widget):
        """Display game log"""
        return LogWindow(
            title="Log for {}".format(self.game),
            buffer=self.game.log_buffer,
            application=self.application
        )

    def on_install_clicked(self, *_args):
        """Install a game"""
        # Install the currently selected game in the UI
        return InstallerWindow(
            parent=self.window,
            game_slug=self.game.slug,
            application=self.application,
        )

    def on_add_manually(self, _widget, *_args):
        """Callback that presents the Add game dialog"""
        AddGameDialog(self.window, game=self.game, runner=self.game.runner_name)

    def on_edit_game_configuration(self, _widget):
        """Edit game preferences"""
        EditGameConfigDialog(self.window, self.game)

    def on_execute_script_clicked(self, _widget):
        """Execute the game's associated script"""
        manual_command = self.game.runner.system_config.get("manual_command")
        if path_exists(manual_command):
            MonitoredCommand(
                [manual_command],
                include_processes=[os.path.basename(manual_command)],
                cwd=self.game.directory,
            ).start()
            logger.info("Running %s in the background", manual_command)

    def on_browse_files(self, _widget):
        """Callback to open a game folder in the file browser"""
        path = self.game.get_browse_dir()
        if not path:
            dialogs.NoticeDialog("This game has no installation directory")
        elif path_exists(path):
            open_uri("file://%s" % path)
        else:
            dialogs.NoticeDialog("Can't open %s \nThe folder doesn't exist." % path)

    def on_create_menu_shortcut(self, *_args):
        """Add the selected game to the system's Games menu."""
        xdgshortcuts.create_launcher(self.game.slug, self.game.id, self.game.name, menu=True)

    def on_create_desktop_shortcut(self, *_args):
        """Create a desktop launcher for the selected game."""
        xdgshortcuts.create_launcher(self.game.slug, self.game.id, self.game.name, desktop=True)

    def on_remove_menu_shortcut(self, *_args):
        """Remove an XDG menu shortcut"""
        xdgshortcuts.remove_launcher(self.game.slug, self.game.id, menu=True)

    def on_remove_desktop_shortcut(self, *_args):
        """Remove a .desktop shortcut"""
        xdgshortcuts.remove_launcher(self.game.slug, self.game.id, desktop=True)

    def on_view_game(self, _widget):
        """Callback to open a game on lutris.net"""
        open_uri("https://lutris.net/games/%s" % self.game.slug)

    def on_remove_game(self, *_args):
        """Callback that present the uninstall dialog to the user"""
        UninstallGameDialog(
            game_id=self.game.id, callback=self.window.remove_game_from_view, parent=self.window
        )
Ejemplo n.º 56
0
class LutrisWindow(Gtk.ApplicationWindow):
    """Handler class for main window signals."""

    __gtype_name__ = 'LutrisWindow'

    main_box = GtkTemplate.Child()
    splash_box = GtkTemplate.Child()
    connect_link = GtkTemplate.Child()
    games_scrollwindow = GtkTemplate.Child()
    sidebar_paned = GtkTemplate.Child()
    sidebar_viewport = GtkTemplate.Child()
    statusbar = GtkTemplate.Child()
    connection_label = GtkTemplate.Child()
    status_box = GtkTemplate.Child()

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

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

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

        view_type = self.get_view_type()
        self.load_icon_type_from_settings(view_type)
        self.filter_installed = \
            settings.read_setting('filter_installed') == 'true'
        self.sidebar_visible = \
            settings.read_setting('sidebar_visible') in ['true', None]
        self.use_dark_theme = settings.read_setting('dark_theme') == 'true'

        # Sync local lutris library with current Steam games and desktop games
        for service in get_services_synced_at_startup():
            service.sync_with_lutris()

        # Window initialization
        self.game_list = pga.get_games()
        self.game_store = GameStore([], self.icon_type, self.filter_installed)
        self.view = self.get_view(view_type)
        super().__init__(default_width=width,
                         default_height=height,
                         icon_name='lutris',
                         application=application,
                         **kwargs)
        if self.maximized:
            self.maximize()
        self.init_template()
        self._init_actions()

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

        # Load view
        self.games_scrollwindow.add(self.view)
        self.connect_signals()
        self.view.show()

        # Contextual menu
        main_entries = [
            ('play', "Play", self.on_game_run),
            ('install', "Install", self.on_install_clicked),
            ('add', "Add manually", self.on_add_manually),
            ('configure', "Configure", self.on_edit_game_configuration),
            ('browse', "Browse files", self.on_browse_files),
            ('desktop-shortcut', "Create desktop shortcut",
             self.create_desktop_shortcut),
            ('rm-desktop-shortcut', "Delete desktop shortcut",
             self.remove_desktop_shortcut),
            ('menu-shortcut', "Create application menu shortcut",
             self.create_menu_shortcut),
            ('rm-menu-shortcut', "Delete application menu shortcut",
             self.remove_menu_shortcut),
            ('install_more', "Install (add) another version",
             self.on_install_clicked),
            ('remove', "Remove", self.on_remove_game),
            ('view', "View on Lutris.net", self.on_view_game),
        ]
        self.menu = ContextualMenu(main_entries)
        self.view.contextual_menu = self.menu

        # Sidebar
        self.sidebar_treeview = SidebarTreeView()
        self.sidebar_treeview.connect('cursor-changed',
                                      self.on_sidebar_changed)
        self.sidebar_viewport.add(self.sidebar_treeview)
        self.sidebar_treeview.show()

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

        self.show_sidebar()
        self.update_runtime()

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

        # Timers
        self.timer_ids = [GLib.timeout_add(300, self.refresh_status)]
        steamapps_paths = steam.get_steamapps_paths(flat=True)
        self.steam_watcher = SteamWatcher(steamapps_paths,
                                          self.on_steam_game_changed)

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

        actions = {
            'browse-games':
            Action(lambda *x: open_uri('https://lutris.net/games/')),
            'register-account':
            Action(lambda *x: open_uri('https://lutris.net/user/register/')),
            'disconnect':
            Action(self.on_disconnect),
            'connect':
            Action(self.on_connect),
            'synchronize':
            Action(lambda *x: self.sync_library()),
            'sync-local':
            Action(lambda *x: self.open_sync_dialog()),
            'add-game':
            Action(self.on_add_game_button_clicked),
            'view-game-log':
            Action(self.on_view_game_log_activate),
            'stop-game':
            Action(self.on_game_stop, enabled=False),
            'start-game':
            Action(self.on_game_run, enabled=False),
            'remove-game':
            Action(self.on_remove_game, enabled=False),
            'preferences':
            Action(self.on_preferences_activate),
            'manage-runners':
            Action(lambda *x: RunnersDialog()),
            'about':
            Action(self.on_about_clicked),
            'show-installed-only':
            Action(self.on_show_installed_state_change,
                   type='b',
                   default=self.filter_installed,
                   accel='<Primary>h'),
            'view-type':
            Action(self.on_viewtype_state_change,
                   type='s',
                   default=self.current_view_type),
            'icon-type':
            Action(self.on_icontype_state_change,
                   type='s',
                   default=self.icon_type),
            'use-dark-theme':
            Action(self.on_dark_theme_state_change,
                   type='b',
                   default=self.use_dark_theme),
            'show-side-bar':
            Action(self.on_sidebar_state_change,
                   type='b',
                   default=self.sidebar_visible,
                   accel='F9'),
        }

        self.actions = {}
        app = self.props.application
        for name, value in actions.items():
            if not value.type:
                action = Gio.SimpleAction.new(name)
                action.connect('activate', value.callback)
            else:
                default_value = None
                param_type = None
                if value.default is not None:
                    default_value = GLib.Variant(value.type, value.default)
                if value.type != 'b':
                    param_type = default_value.get_type()
                action = Gio.SimpleAction.new_stateful(name, param_type,
                                                       default_value)
                action.connect('change-state', value.callback)
            self.actions[name] = action
            if value.enabled is False:
                action.props.enabled = False
            self.add_action(action)
            if value.accel:
                app.add_accelerator(value.accel, 'win.' + name)

    @property
    def current_view_type(self):
        return 'grid' if isinstance(self.view, GameGridView) else 'list'

    def on_steam_game_changed(self, operation, path):
        appmanifest = steam.AppManifest(path)
        if self.running_game and 'steam' in self.running_game.runner_name:
            self.running_game.notify_steam_game_changed(appmanifest)

        runner_name = appmanifest.get_runner_name()
        games = pga.get_games_where(steamid=appmanifest.steamid)
        if operation == Gio.FileMonitorEvent.DELETED:
            for game in games:
                if game['runner'] == runner_name:
                    steam.mark_as_uninstalled(game)
                    self.view.set_uninstalled(Game(game['id']))
                    break
        elif operation in (Gio.FileMonitorEvent.CHANGED,
                           Gio.FileMonitorEvent.CREATED):
            if not appmanifest.is_installed():
                return
            if runner_name == 'winesteam':
                return
            game_info = None
            for game in games:
                if game['installed'] == 0:
                    game_info = game
                else:
                    # Game is already installed, don't do anything
                    return
            if not game_info:
                game_info = {
                    'name': appmanifest.name,
                    'slug': appmanifest.slug,
                }
            game_id = steam.mark_as_installed(appmanifest.steamid, runner_name,
                                              game_info)
            game_ids = [game['id'] for game in self.game_list]
            if game_id not in game_ids:
                self.add_game_to_view(game_id)
            else:
                self.view.set_installed(Game(game_id))

    @staticmethod
    def set_dark_theme(is_dark):
        gtksettings = Gtk.Settings.get_default()
        gtksettings.set_property("gtk-application-prefer-dark-theme", is_dark)

    def get_view(self, view_type):
        if view_type == 'grid':
            return GameGridView(self.game_store)
        else:
            return GameListView(self.game_store)

    def connect_signals(self):
        """Connect signals from the view with the main window.

        This must be called each time the view is rebuilt.
        """
        self.view.connect('game-installed', self.on_game_installed)
        self.view.connect("game-activated", self.on_game_run)
        self.view.connect("game-selected", self.game_selection_changed)
        self.view.connect("remove-game", self.on_remove_game)

    @staticmethod
    def check_update():
        """Verify availability of client update."""
        version_request = http.Request('https://lutris.net/version')
        version_request.get()
        version = version_request.content
        if version:
            latest_version = settings.read_setting('latest_version')
            if version > (latest_version or settings.VERSION):
                dialogs.ClientUpdateDialog()
                # Store latest version seen to avoid showing
                # the dialog more than once.
                settings.write_setting('latest_version', version)

    @staticmethod
    def get_view_type():
        view_type = settings.read_setting('view_type')
        if view_type in ['grid', 'list']:
            return view_type
        return settings.GAME_VIEW

    def load_icon_type_from_settings(self, view_type):
        """Return the icon style depending on the type of view."""
        if view_type == 'list':
            self.icon_type = settings.read_setting('icon_type_listview')
            default = settings.ICON_TYPE_LISTVIEW
        else:
            self.icon_type = settings.read_setting('icon_type_gridview')
            default = settings.ICON_TYPE_GRIDVIEW
        if self.icon_type not in ("banner_small", "banner", "icon",
                                  "icon_small"):
            self.icon_type = default
        return self.icon_type

    def switch_splash_screen(self):
        if len(self.game_list) == 0:
            self.splash_box.show()
            self.sidebar_paned.hide()
            self.games_scrollwindow.hide()
        else:
            self.splash_box.hide()
            self.sidebar_paned.show()
            self.games_scrollwindow.show()

    def switch_view(self, view_type):
        """Switch between grid view and list view."""
        self.view.destroy()
        self.load_icon_type_from_settings(view_type)
        self.game_store.set_icon_type(self.icon_type)

        self.view = self.get_view(view_type)
        self.view.contextual_menu = self.menu
        self.connect_signals()
        scrollwindow_children = self.games_scrollwindow.get_children()
        if len(scrollwindow_children):
            child = scrollwindow_children[0]
            child.destroy()
        self.games_scrollwindow.add(self.view)
        self.set_selected_filter(self.selected_runner, self.selected_platform)
        self.set_show_installed_state(self.filter_installed)
        self.view.show_all()

        settings.write_setting('view_type', view_type)

    def sync_library(self):
        """Synchronize games with local stuff and server."""
        def update_gui(result, error):
            if result:
                added_ids, updated_ids = result

                # sqlite limits the number of query parameters to 999, to
                # bypass that limitation, divide the query in chunks
                page_size = 999
                added_games = chain.from_iterable([
                    pga.get_games_where(
                        id__in=list(added_ids)[p * page_size:p * page_size +
                                               page_size])
                    for p in range(math.ceil(len(added_ids) / page_size))
                ])
                self.game_list += added_games
                self.view.populate_games(added_games)
                self.switch_splash_screen()
                GLib.idle_add(self.update_existing_games, added_ids,
                              updated_ids, True)
            else:
                logger.error("No results returned when syncing the library")

        self.set_status("Syncing library")
        AsyncCall(sync_from_remote, update_gui)

    def open_sync_dialog(self):
        sync_dialog = SyncServiceDialog(parent=self)
        sync_dialog.run()

    def update_existing_games(self, added, updated, first_run=False):
        for game_id in updated.difference(added):
            # XXX this migth not work if the game has no 'item' set
            self.view.update_row(pga.get_game_by_field(game_id, 'id'))

        if first_run:
            icons_sync = AsyncCall(self.sync_icons, callback=None)
            self.threads_stoppers.append(icons_sync.stop_request.set)
            self.set_status("")

    def update_runtime(self):
        self.runtime_updater.update(self.set_status)
        self.threads_stoppers += self.runtime_updater.cancellables

    def sync_icons(self):
        resources.fetch_icons([game['slug'] for game in self.game_list],
                              callback=self.on_image_downloaded)

    def set_status(self, text):
        for child_widget in self.status_box.get_children():
            child_widget.destroy()
        label = Gtk.Label(text)
        label.show()
        self.status_box.add(label)

    def refresh_status(self):
        """Refresh status bar."""
        if self.running_game:
            name = self.running_game.name
            if self.running_game.state == self.running_game.STATE_IDLE:
                self.set_status("Preparing to launch %s" % name)
            elif self.running_game.state == self.running_game.STATE_STOPPED:
                self.set_status("Game has quit")
                self.actions['stop-game'].props.enabled = False
            elif self.running_game.state == self.running_game.STATE_RUNNING:
                self.set_status("Playing %s" % name)
                self.actions['stop-game'].props.enabled = True
        return True

    # ---------
    # Callbacks
    # ---------

    def on_dark_theme_state_change(self, action, value):
        action.set_state(value)
        self.use_dark_theme = value.get_boolean()
        setting_value = 'true' if self.use_dark_theme else 'false'
        settings.write_setting('dark_theme', setting_value)
        self.set_dark_theme(self.use_dark_theme)

    @GtkTemplate.Callback
    def on_connect(self, *args):
        """Callback when a user connects to his account."""
        login_dialog = dialogs.ClientLoginDialog(self)
        login_dialog.connect('connected', self.on_connect_success)
        return True

    def on_connect_success(self, dialog, credentials):
        if isinstance(credentials, str):
            username = credentials
        else:
            username = credentials["username"]
        self.toggle_connection(True, username)
        self.sync_library()
        self.connect_link.hide()
        self.actions['synchronize'].props.enabled = True

    @GtkTemplate.Callback
    def on_disconnect(self, *args):
        api.disconnect()
        self.toggle_connection(False)
        self.connect_link.show()
        self.actions['synchronize'].props.enabled = False

    def toggle_connection(self, is_connected, username=None):
        self.props.application.set_connect_state(is_connected)
        if is_connected:
            connection_status = username
            logger.info('Connected to lutris.net as %s', connection_status)
        else:
            connection_status = "Not connected"
        self.connection_label.set_text(connection_status)

    @GtkTemplate.Callback
    def on_resize(self, widget, *args):
        """Size-allocate signal.

        Updates stored window size and maximized state.
        """
        if not widget.get_window():
            return
        self.maximized = widget.is_maximized()
        if not self.maximized:
            self.window_size = widget.get_size()

    @GtkTemplate.Callback
    def on_destroy(self, *args):
        """Signal for window close."""
        # Stop cancellable running threads
        for stopper in self.threads_stoppers:
            stopper()
        self.steam_watcher = None

        if self.running_game \
           and self.running_game.state != self.running_game.STATE_STOPPED:
            logger.info("%s is still running, stopping it",
                        self.running_game.name)
            self.running_game.stop()

        # Save settings
        width, height = self.window_size
        settings.write_setting('width', width)
        settings.write_setting('height', height)
        settings.write_setting('maximized', self.maximized)

    @GtkTemplate.Callback
    def on_preferences_activate(self, *args):
        """Callback when preferences is activated."""
        SystemConfigDialog(parent=self)

    def on_show_installed_state_change(self, action, value):
        action.set_state(value)
        filter_installed = value.get_boolean()
        self.set_show_installed_state(filter_installed)

    def set_show_installed_state(self, filter_installed):
        self.filter_installed = filter_installed
        setting_value = 'true' if filter_installed else 'false'
        settings.write_setting('filter_installed', setting_value)
        self.game_store.filter_installed = filter_installed
        self.game_store.modelfilter.refilter()

    @GtkTemplate.Callback
    def on_pga_menuitem_activate(self, *args):
        dialogs.PgaSourceDialog(parent=self)

    @GtkTemplate.Callback
    def on_search_entry_changed(self, widget):
        self.game_store.filter_text = widget.get_text()
        self.game_store.modelfilter.refilter()

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

    def _get_current_game_id(self):
        """Return the id of the current selected game while taking care of the
        double clic bug.
        """
        # Wait two seconds to avoid running a game twice
        if time.time() - self.game_launch_time < 2:
            return
        self.game_launch_time = time.time()
        return self.view.selected_game

    def on_game_run(self, *args, game_id=None):
        """Launch a game, or install it if it is not"""
        if not game_id:
            game_id = self._get_current_game_id()
        if not game_id:
            return
        self.running_game = Game(game_id)
        if self.running_game.is_installed:
            self.running_game.play()
        else:
            game_slug = self.running_game.slug
            self.running_game = None
            InstallerDialog(game_slug=game_slug, parent=self)

    @GtkTemplate.Callback
    def on_game_stop(self, *args):
        """Stop running game."""
        if self.running_game:
            self.running_game.stop()
            self.actions['stop-game'].props.enabled = False

    def on_install_clicked(self,
                           *args,
                           game_slug=None,
                           installer_file=None,
                           revision=None):
        """Install a game"""

        installer_desc = game_slug if game_slug else installer_file
        if revision:
            installer_desc += " (%s)" % revision
        logger.info("Installing %s" % installer_desc)

        if not game_slug and not installer_file:
            # Install the currently selected game in the UI
            game_id = self._get_current_game_id()
            game = pga.get_game_by_field(game_id, 'id')
            game_slug = game.get('slug')
        if not game_slug and not installer_file:
            return
        InstallerDialog(game_slug=game_slug,
                        installer_file=installer_file,
                        revision=revision,
                        parent=self)

    def game_selection_changed(self, _widget):
        # Emulate double click to workaround GTK bug #484640
        # https://bugzilla.gnome.org/show_bug.cgi?id=484640
        if isinstance(self.view, GameGridView):
            is_double_click = time.time() - self.game_selection_time < 0.4
            is_same_game = self.view.selected_game == self.last_selected_game
            if is_double_click and is_same_game:
                self.on_game_run()
            self.game_selection_time = time.time()
            self.last_selected_game = self.view.selected_game

        sensitive = True if self.view.selected_game else False
        self.actions['start-game'].props.enabled = sensitive
        self.actions['remove-game'].props.enabled = sensitive

    def on_game_installed(self, view, game_id):
        if type(game_id) != int:
            raise ValueError("game_id must be an int")
        if not self.view.has_game_id(game_id):
            logger.debug("Adding new installed game to view (%d)" % game_id)
            self.add_game_to_view(game_id, async=False)

        game = Game(game_id)
        view.set_installed(game)
        self.sidebar_treeview.update()
        GLib.idle_add(resources.fetch_icons, [game.slug],
                      self.on_image_downloaded)

    def on_image_downloaded(self, game_slugs):
        logger.debug("Updated images for %d games" % len(game_slugs))

        for game_slug in game_slugs:
            games = pga.get_games_where(slug=game_slug)
            for game in games:
                game = Game(game['id'])
                is_installed = game.is_installed
                self.view.update_image(game.id, is_installed)

    def on_add_manually(self, widget, *args):
        def on_game_added(game):
            self.view.set_installed(game)
            self.sidebar_treeview.update()

        game = Game(self.view.selected_game)
        AddGameDialog(self,
                      game=game,
                      runner=self.selected_runner,
                      callback=lambda: on_game_added(game))

    @GtkTemplate.Callback
    def on_view_game_log_activate(self, *args):
        if not self.running_game:
            dialogs.ErrorDialog('No game log available', parent=self)
            return
        log_title = u"Log for {}".format(self.running_game)
        log_window = LogWindow(title=log_title,
                               buffer=self.running_game.log_buffer,
                               parent=self)
        log_window.run()
        log_window.destroy()

    @GtkTemplate.Callback
    def on_add_game_button_clicked(self, *args):
        """Add a new game manually with the AddGameDialog."""
        dialog = AddGameDialog(
            self,
            runner=self.selected_runner,
            callback=lambda: self.add_game_to_view(dialog.game.id))
        return True

    def add_game_to_view(self, game_id, async=True):
        if not game_id:
            raise ValueError("Missing game id")

        def do_add_game():
            self.view.add_game_by_id(game_id)
            self.switch_splash_screen()
            self.sidebar_treeview.update()
            return False

        if async:
            GLib.idle_add(do_add_game)
        else:
            do_add_game()
Ejemplo n.º 57
0
class LutrisWindow(object):
    """Handler class for main window signals."""

    def __init__(self):

        ui_filename = os.path.join(datapath.get(), "ui", "LutrisWindow.ui")
        if not os.path.exists(ui_filename):
            raise IOError("File %s not found" % ui_filename)

        self.running_game = None
        self.threads_stoppers = []

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

        self.builder = Gtk.Builder()
        self.builder.add_from_file(ui_filename)

        # Load settings
        width = int(settings.read_setting("width") or 800)
        height = int(settings.read_setting("height") or 600)
        self.window_size = (width, height)
        view_type = self.get_view_type()
        self.icon_type = self.get_icon_type(view_type)
        filter_installed_setting = settings.read_setting("filter_installed") or "false"
        filter_installed = filter_installed_setting == "true"
        show_installed_games_menuitem = self.builder.get_object("filter_installed")
        show_installed_games_menuitem.set_active(filter_installed)

        # Load view
        logger.debug("Loading view")
        self.game_store = GameStore([], self.icon_type, filter_installed)
        self.view = load_view(view_type, self.game_store)

        logger.debug("Connecting signals")
        self.main_box = self.builder.get_object("main_box")
        self.splash_box = self.builder.get_object("splash_box")
        # View menu
        self.grid_view_menuitem = self.builder.get_object("gridview_menuitem")
        self.grid_view_menuitem.set_active(view_type == "grid")
        self.list_view_menuitem = self.builder.get_object("listview_menuitem")
        self.list_view_menuitem.set_active(view_type == "list")
        # View buttons
        self.grid_view_btn = self.builder.get_object("switch_grid_view_btn")
        self.grid_view_btn.set_active(view_type == "grid")
        self.list_view_btn = self.builder.get_object("switch_list_view_btn")
        self.list_view_btn.set_active(view_type == "list")
        # Icon type menu
        self.banner_small_menuitem = self.builder.get_object("banner_small_menuitem")
        self.banner_small_menuitem.set_active(self.icon_type == "banner_small")
        self.banner_menuitem = self.builder.get_object("banner_menuitem")
        self.banner_menuitem.set_active(self.icon_type == "banner")
        self.icon_menuitem = self.builder.get_object("icon_menuitem")
        self.icon_menuitem.set_active(self.icon_type == "icon")

        self.search_entry = self.builder.get_object("search_entry")
        self.search_entry.connect("icon-press", self.on_clear_search)

        # Scroll window
        self.games_scrollwindow = self.builder.get_object("games_scrollwindow")
        self.games_scrollwindow.add(self.view)
        # Status bar
        self.status_label = self.builder.get_object("status_label")
        self.joystick_icons = []
        # Buttons
        self.stop_button = self.builder.get_object("stop_button")
        self.stop_button.set_sensitive(False)
        self.delete_button = self.builder.get_object("delete_button")
        self.delete_button.set_sensitive(False)
        self.play_button = self.builder.get_object("play_button")
        self.play_button.set_sensitive(False)

        # Contextual menu
        main_entries = [
            ("play", "Play", self.on_game_run),
            ("install", "Install", self.on_install_clicked),
            ("add", "Add manually", self.add_manually),
            ("configure", "Configure", self.edit_game_configuration),
            ("browse", "Browse files", self.on_browse_files),
            ("desktop-shortcut", "Create desktop shortcut", self.create_desktop_shortcut),
            ("rm-desktop-shortcut", "Delete desktop shortcut", self.remove_desktop_shortcut),
            ("menu-shortcut", "Create application menu shortcut", self.create_menu_shortcut),
            ("rm-menu-shortcut", "Delete application menu shortcut", self.remove_menu_shortcut),
            ("remove", "Remove", self.on_remove_game),
        ]
        self.menu = ContextualMenu(main_entries)
        self.view.contextual_menu = self.menu

        # Sidebar
        sidebar_paned = self.builder.get_object("sidebar_paned")
        sidebar_paned.set_position(150)
        self.sidebar_treeview = SidebarTreeView()
        self.sidebar_treeview.connect("cursor-changed", self.on_sidebar_changed)
        self.sidebar_viewport = self.builder.get_object("sidebar_viewport")
        self.sidebar_viewport.add(self.sidebar_treeview)

        # Window initialization
        self.window = self.builder.get_object("window")
        self.window.resize_to_geometry(width, height)
        self.window.show_all()
        self.builder.connect_signals(self)
        self.connect_signals()

        # XXX Hide PGA config menu item until it actually gets implemented
        pga_menuitem = self.builder.get_object("pga_menuitem")
        pga_menuitem.hide()

        self.init_game_store()

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

        # Timers
        self.timer_ids = [GLib.timeout_add(300, self.refresh_status), GLib.timeout_add(10000, self.on_sync_timer)]

    def init_game_store(self):
        logger.debug("Getting game list")
        game_list = get_game_list()
        self.game_store.fill_store(game_list)
        self.switch_splash_screen()

    @property
    def current_view_type(self):
        return "grid" if self.view.__class__.__name__ == "GameGridView" else "list"

    def connect_signals(self):
        """Connect signals from the view with the main window.

        This must be called each time the view is rebuilt.
        """
        self.view.connect("game-installed", self.on_game_installed)
        self.view.connect("game-activated", self.on_game_run)
        self.view.connect("game-selected", self.game_selection_changed)
        self.window.connect("configure-event", self.on_resize)
        self.window.connect("key-press-event", self.on_keypress)

    def get_view_type(self):
        view_type = settings.read_setting("view_type")
        if view_type in ["grid", "list"]:
            return view_type
        return settings.GAME_VIEW

    def get_icon_type(self, view_type):
        """Return the icon style depending on the type of view."""
        if view_type == "list":
            icon_type = settings.read_setting("icon_type_listview")
            default = settings.ICON_TYPE_LISTVIEW
        else:
            icon_type = settings.read_setting("icon_type_gridview")
            default = settings.ICON_TYPE_GRIDVIEW
        if icon_type not in ("banner_small", "banner", "icon"):
            icon_type = default
        return icon_type

    def switch_splash_screen(self):
        if not pga.get_table_length():
            self.splash_box.show()
            self.games_scrollwindow.hide()
            self.sidebar_viewport.hide()
        else:
            self.splash_box.hide()
            self.games_scrollwindow.show()
            self.sidebar_viewport.show()

    def switch_view(self, view_type):
        """Switch between grid view and list view."""
        logger.debug("Switching view")
        if view_type == self.get_view_type():
            return
        self.view.destroy()
        icon_type = self.get_icon_type(view_type)
        self.game_store.set_icon_type(icon_type)

        self.view = load_view(view_type, self.game_store)
        self.view.contextual_menu = self.menu
        self.connect_signals()
        self.games_scrollwindow.add(self.view)
        self.view.show_all()

        # Note: set_active(True *or* False) apparently makes ALL the menuitems
        # in the group send the activate signal...
        if icon_type == "banner_small":
            self.banner_small_menuitem.set_active(True)
        if icon_type == "icon":
            self.icon_menuitem.set_active(True)
        if icon_type == "banner":
            self.banner_menuitem.set_active(True)
        settings.write_setting("view_type", view_type)

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

        def update_gui(result, error):
            added, updated, installed, uninstalled = result
            self.switch_splash_screen()
            self.game_store.fill_store(added)

            GLib.idle_add(self.update_existing_games, added, updated, installed, uninstalled, True)

        self.set_status("Syncing library")
        AsyncCall(Sync().sync_all, update_gui)

    def update_existing_games(self, added, updated, installed, uninstalled, first_run=False):
        for game_id in updated.difference(added):
            self.view.update_row(pga.get_game_by_field(game_id, "id"))

        for game_id in installed.difference(added):
            if not self.view.get_row_by_id(game_id):
                self.view.add_game(game_id)
            self.view.set_installed(Game(game_id))

        for game_id in uninstalled.difference(added):
            self.view.set_uninstalled(game_id)

        self.sidebar_treeview.update()

        if first_run:
            icons_sync = AsyncCall(self.sync_icons, None, stoppable=True)
            self.threads_stoppers.append(icons_sync.stop_request.set)
            self.set_status("Library synced")
            self.update_runtime()

    def update_runtime(self):
        cancellables = runtime.update(self.set_status)
        self.threads_stoppers += cancellables

    def sync_icons(self, stop_request=None):
        resources.fetch_icons(
            [game for game in pga.get_games()], callback=self.on_image_downloaded, stop_request=stop_request
        )

    def set_status(self, text):
        self.status_label.set_text(text)

    def refresh_status(self):
        """Refresh status bar."""
        if self.running_game:
            name = self.running_game.name
            if self.running_game.state == self.running_game.STATE_IDLE:
                self.set_status("Preparing to launch %s" % name)
            elif self.running_game.state == self.running_game.STATE_STOPPED:
                self.set_status("Game has quit")
                display.set_cursor("default", self.window.get_window())
                self.stop_button.set_sensitive(False)
            elif self.running_game.state == self.running_game.STATE_RUNNING:
                self.set_status("Playing %s" % name)
                display.set_cursor("default", self.window.get_window())
                self.stop_button.set_sensitive(True)
        for index in range(4):
            self.joystick_icons.append(self.builder.get_object("js" + str(index) + "image"))
            if os.path.exists("/dev/input/js%d" % index):
                self.joystick_icons[index].set_visible(True)
            else:
                self.joystick_icons[index].set_visible(False)
        return True

    def about(self, _widget, _data=None):
        """Open the about dialog."""
        dialogs.AboutDialog()

    # Callbacks
    def on_clear_search(self, widget, icon_pos, event):
        if icon_pos == Gtk.EntryIconPosition.SECONDARY:
            widget.set_text("")

    def on_connect(self, *args):
        """Callback when a user connects to his account."""
        login_dialog = dialogs.ClientLoginDialog()
        login_dialog.connect("connected", self.on_connect_success)

    def on_connect_success(self, dialog, credentials):
        if isinstance(credentials, str):
            username = credentials
        else:
            username = credentials["username"]
        self.toggle_connection(True, username)
        self.sync_library()

    def on_disconnect(self, *args):
        api.disconnect()
        self.toggle_connection(False)

    def toggle_connection(self, is_connected, username=None):
        disconnect_menuitem = self.builder.get_object("disconnect_menuitem")
        connect_menuitem = self.builder.get_object("connect_menuitem")
        connection_label = self.builder.get_object("connection_label")

        if is_connected:
            disconnect_menuitem.show()
            connect_menuitem.hide()
            connection_status = "Connected as %s" % username
        else:
            disconnect_menuitem.hide()
            connect_menuitem.show()
            connection_status = "Not connected"
        logger.info(connection_status)
        connection_label.set_text(connection_status)

    def on_register_account(self, *args):
        Gtk.show_uri(None, "http://lutris.net/user/register", Gdk.CURRENT_TIME)

    def on_synchronize_manually(self, *args):
        """Callback when Synchronize Library is activated."""
        self.sync_library()

    def on_sync_timer(self):
        if not self.running_game or self.running_game.state == Game.STATE_STOPPED:

            def update_gui(result, error):
                self.update_existing_games(set(), set(), *result)

            AsyncCall(Sync().sync_local, update_gui)
        return True

    def on_resize(self, widget, *args):
        self.window_size = widget.get_size()

    def on_destroy(self, *args):
        """Signal for window close."""
        # Stop cancellable running threads
        for stopper in self.threads_stoppers:
            stopper()

        # Save settings
        width, height = self.window_size
        settings.write_setting("width", width)
        settings.write_setting("height", height)

        Gtk.main_quit(*args)
        logger.debug("Quitting lutris")

    def on_runners_activate(self, _widget, _data=None):
        """Callback when manage runners is activated."""
        RunnersDialog()

    def on_preferences_activate(self, _widget, _data=None):
        """Callback when preferences is activated."""
        SystemConfigDialog()

    def on_show_installed_games_toggled(self, widget, data=None):
        filter_installed = widget.get_active()
        setting_value = "true" if filter_installed else "false"
        settings.write_setting("filter_installed", setting_value)
        self.game_store.filter_installed = filter_installed
        self.game_store.modelfilter.refilter()

    def on_pga_menuitem_activate(self, _widget, _data=None):
        dialogs.PgaSourceDialog()

    def on_search_entry_changed(self, widget):
        self.game_store.filter_text = widget.get_text()
        self.game_store.modelfilter.refilter()

    def _get_current_game_id(self):
        """Return the id of the current selected game while taking care of the
        double clic bug.
        """
        # Wait two seconds to avoid running a game twice
        if time.time() - self.game_launch_time < 2:
            return
        self.game_launch_time = time.time()
        return self.view.selected_game

    def on_game_run(self, _widget=None, game_id=None):
        """Launch a game, or install it if it is not"""
        if not game_id:
            game_id = self._get_current_game_id()
        if not game_id:
            return
        display.set_cursor("wait", self.window.get_window())
        self.running_game = Game(game_id)
        if self.running_game.is_installed:
            self.running_game.play()
        else:
            game_slug = self.running_game.slug
            self.running_game = None
            InstallerDialog(game_slug, self)

    def on_game_stop(self, *args):
        """Stop running game."""
        if self.running_game:
            self.running_game.stop()
            self.stop_button.set_sensitive(False)

    def on_install_clicked(self, _widget=None, game_ref=None):
        """Install a game"""
        if not game_ref:
            game_id = self._get_current_game_id()
            game = pga.get_game_by_field(game_id, "id")
            game_ref = game.get("slug")
            logger.debug("Installing game %s (%s)" % (game_ref, game_id))
        if not game_ref:
            return
        display.set_cursor("wait", self.window.get_window())
        InstallerDialog(game_ref, self)

    def on_keypress(self, widget, event):
        if event.keyval == Gdk.KEY_F9:
            self.toggle_sidebar()

    def game_selection_changed(self, _widget):
        # Emulate double click to workaround GTK bug #484640
        # https://bugzilla.gnome.org/show_bug.cgi?id=484640
        if type(self.view) is GameGridView:
            is_double_click = time.time() - self.game_selection_time < 0.4
            is_same_game = self.view.selected_game == self.last_selected_game
            if is_double_click and is_same_game:
                self.on_game_run()
            self.game_selection_time = time.time()
            self.last_selected_game = self.view.selected_game

        sensitive = True if self.view.selected_game else False
        self.play_button.set_sensitive(sensitive)
        self.delete_button.set_sensitive(sensitive)

    def on_game_installed(self, view, game_id):
        if not self.view.get_row_by_id(game_id):
            logger.debug("Adding new installed game to view (%s)" % game_id)
            self.add_game_to_view(game_id)
        view.set_installed(Game(game_id))
        self.sidebar_treeview.update()

    def on_image_downloaded(self, game_id):
        game = Game(game_id)
        is_installed = game.is_installed
        self.view.update_image(game_id, is_installed)

    def add_manually(self, *args):
        game = Game(self.view.selected_game)
        add_game_dialog = AddGameDialog(self.window, game)
        add_game_dialog.run()
        if add_game_dialog.saved:
            self.view.set_installed(game)
            self.sidebar_treeview.update()

    def on_view_game_log_activate(self, widget):
        if not self.running_game:
            dialogs.ErrorDialog("No game log available")
            return
        log_title = u"Log for {}".format(self.running_game)
        log_window = LogWindow(log_title, self.window)
        log_window.logtextview.set_text(self.running_game.game_log)
        log_window.run()
        log_window.destroy()

    def add_game(self, _widget, _data=None):
        """Add a new game."""
        add_game_dialog = AddGameDialog(self.window)
        add_game_dialog.run()
        if add_game_dialog.saved:
            self.add_game_to_view(add_game_dialog.game.id)

    def add_game_to_view(self, game_id):
        if not game_id:
            raise ValueError("Missing game id")

        def do_add_game():
            self.view.add_game(game_id)
            self.switch_splash_screen()
            self.sidebar_treeview.update()

        GLib.idle_add(do_add_game)

    def on_remove_game(self, _widget, _data=None):
        selected_game = self.view.selected_game
        UninstallGameDialog(game_id=selected_game, callback=self.remove_game_from_view)

    def remove_game_from_view(self, game_id, from_library=False):
        def do_remove_game():
            self.view.remove_game(game_id)
            self.switch_splash_screen()

        if from_library:
            GLib.idle_add(do_remove_game)
        else:
            self.view.update_image(game_id, is_installed=False)
        self.sidebar_treeview.update()

    def on_browse_files(self, widget):
        game = Game(self.view.selected_game)
        path = game.get_browse_dir()
        if path and os.path.exists(path):
            Gtk.show_uri(None, "file://" + path, Gdk.CURRENT_TIME)
        else:
            dialogs.NoticeDialog("Can't open %s \nThe folder doesn't exist." % path)

    def edit_game_configuration(self, _button):
        """Edit game preferences."""

        def on_dialog_saved():
            game_id = dialog.game.id
            self.view.remove_game(game_id)
            self.view.add_game(game_id)
            self.view.set_selected_game(game_id)
            self.sidebar_treeview.update()

        game = Game(self.view.selected_game)
        if game.is_installed:
            dialog = EditGameConfigDialog(self.window, game, on_dialog_saved)

    def on_viewmenu_toggled(self, menuitem):
        view_type = "grid" if menuitem.get_active() else "list"
        if view_type == self.current_view_type:
            return
        self.switch_view(view_type)
        self.grid_view_btn.set_active(view_type == "grid")
        self.list_view_btn.set_active(view_type == "list")

    def on_viewbtn_toggled(self, widget):
        view_type = "grid" if widget.get_active() else "list"
        if view_type == self.current_view_type:
            return
        self.switch_view(view_type)
        self.grid_view_menuitem.set_active(view_type == "grid")
        self.list_view_menuitem.set_active(view_type == "list")

    def on_icon_type_activate(self, menuitem):
        icon_type = menuitem.get_name()
        if icon_type == self.game_store.icon_type or not menuitem.get_active():
            return
        if self.current_view_type == "grid":
            settings.write_setting("icon_type_gridview", icon_type)
        elif self.current_view_type == "list":
            settings.write_setting("icon_type_listview", icon_type)
        self.game_store.set_icon_type(icon_type)

    def create_menu_shortcut(self, *args):
        """Add the selected game to the system's Games menu."""
        game = Game(self.view.selected_game).name
        shortcuts.create_launcher(game.slug, game.id, game.name, menu=True)

    def create_desktop_shortcut(self, *args):
        """Create a desktop launcher for the selected game."""
        game = Game(self.view.selected_game)
        shortcuts.create_launcher(game.slug, game.id, game.name, desktop=True)

    def remove_menu_shortcut(self, *args):
        game = Game(self.view.selected_game)
        shortcuts.remove_launcher(game.slug, game.id, menu=True)

    def remove_desktop_shortcut(self, *args):
        game = Game(self.view.selected_game)
        shortcuts.remove_launcher(game.slug, game.id, desktop=True)

    def toggle_sidebar(self):
        if self.sidebar_viewport.is_visible():
            self.sidebar_viewport.hide()
        else:
            self.sidebar_viewport.show()

    def on_sidebar_changed(self, widget):
        self.view.game_store.filter_runner = widget.get_selected_runner()
        self.game_store.modelfilter.refilter()
Ejemplo n.º 58
0
 def on_view_game(self, widget):
     game = Game(self.view.selected_game)
     open_uri('https://lutris.net/games/' + game.slug)
Ejemplo n.º 59
0
 def launch_game(self, widget, _data=None):
     """Launch a game after it's been installed"""
     widget.set_sensitive(False)
     game = Game(self.interpreter.game_slug)
     game.play()
     self.close(widget)
Ejemplo n.º 60
0
class GameDialogCommon(Dialog):
    """Base class for config dialogs"""
    no_runner_label = _("Select a runner in the Game Info tab")

    def __init__(self, title, parent=None):
        super().__init__(title, parent=parent)
        self.set_type_hint(Gdk.WindowTypeHint.NORMAL)
        self.set_default_size(DIALOG_WIDTH, DIALOG_HEIGHT)
        self.notebook = None
        self.name_entry = None
        self.runner_box = None

        self.timer_id = None
        self.game = None
        self.saved = None
        self.slug = None
        self.slug_entry = None
        self.directory_entry = None
        self.year_entry = None
        self.slug_change_button = None
        self.runner_dropdown = None
        self.banner_button = None
        self.icon_button = None
        self.game_box = None
        self.system_box = None
        self.runner_name = None
        self.runner_index = None
        self.lutris_config = None

        # These are independent windows, but start centered over
        # a parent like a dialog. Not modal, not really transient,
        # and does not share modality with other windows - so it
        # needs its own window group.
        Gtk.WindowGroup().add_window(self)
        GLib.idle_add(self.clear_transient_for)

    def clear_transient_for(self):
        # we need the parent set to be centered over the parent, but
        # we don't want to be transient really- we want other windows
        # able to come to the front.
        self.set_transient_for(None)
        return False

    @staticmethod
    def build_scrolled_window(widget):
        """Return a scrolled window containing config widgets"""
        scrolled_window = Gtk.ScrolledWindow(visible=True)
        scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled_window.add(widget)
        return scrolled_window

    def build_notebook(self):
        self.notebook = Gtk.Notebook(visible=True)
        self.notebook.set_show_border(False)
        self.vbox.pack_start(self.notebook, True, True, 10)

    def build_tabs(self, config_level):
        """Build tabs (for game and runner levels)"""
        self.timer_id = None
        if config_level == "game":
            self._build_info_tab()
            self._build_game_tab()
        self._build_runner_tab(config_level)
        self._build_system_tab(config_level)

    def _build_info_tab(self):
        info_box = VBox()

        if self.game:
            info_box.pack_start(self._get_banner_box(), False, False, 6)  # Banner

        info_box.pack_start(self._get_name_box(), False, False, 6)  # Game name

        self.runner_box = self._get_runner_box()
        info_box.pack_start(self.runner_box, False, False, 6)  # Runner

        info_box.pack_start(self._get_year_box(), False, False, 6)  # Year

        if self.game:
            info_box.pack_start(self._get_slug_box(), False, False, 6)
            info_box.pack_start(self._get_directory_box(), False, False, 6)

        info_sw = self.build_scrolled_window(info_box)
        self._add_notebook_tab(info_sw, _("Game info"))

    def _get_name_box(self):
        box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)
        label = Label(_("Name"))
        box.pack_start(label, False, False, 0)
        self.name_entry = Gtk.Entry()
        if self.game:
            self.name_entry.set_text(self.game.name)
        box.pack_start(self.name_entry, True, True, 0)
        return box

    def _get_slug_box(self):
        slug_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)

        label = Label(_("Identifier"))
        slug_box.pack_start(label, False, False, 0)

        self.slug_entry = SlugEntry()
        self.slug_entry.set_text(self.game.slug)
        self.slug_entry.set_sensitive(False)
        self.slug_entry.connect("activate", self.on_slug_entry_activate)
        slug_box.pack_start(self.slug_entry, True, True, 0)

        self.slug_change_button = Gtk.Button(_("Change"))
        self.slug_change_button.connect("clicked", self.on_slug_change_clicked)
        slug_box.pack_start(self.slug_change_button, False, False, 0)

        return slug_box

    def _get_directory_box(self):
        """Return widget displaying the location of the game and allowing to move it"""
        box = Gtk.Box(spacing=12, margin_right=12, margin_left=12, visible=True)
        label = Label(_("Directory"))
        box.pack_start(label, False, False, 0)
        self.directory_entry = Gtk.Entry(visible=True)
        self.directory_entry.set_text(self.game.directory)
        self.directory_entry.set_sensitive(False)
        box.pack_start(self.directory_entry, True, True, 0)
        move_button = Gtk.Button(_("Move"), visible=True)
        move_button.connect("clicked", self.on_move_clicked)
        box.pack_start(move_button, False, False, 0)
        return box

    def _get_runner_box(self):
        runner_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)

        runner_label = Label(_("Runner"))
        runner_box.pack_start(runner_label, False, False, 0)

        self.runner_dropdown = self._get_runner_dropdown()
        runner_box.pack_start(self.runner_dropdown, True, True, 0)

        return runner_box

    def _get_banner_box(self):
        banner_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)

        label = Label("")
        banner_box.pack_start(label, False, False, 0)

        self.banner_button = Gtk.Button()
        self._set_image("banner")
        self.banner_button.connect("clicked", self.on_custom_image_select, "banner")
        banner_box.pack_start(self.banner_button, False, False, 0)

        reset_banner_button = Gtk.Button.new_from_icon_name("edit-clear", Gtk.IconSize.MENU)
        reset_banner_button.set_relief(Gtk.ReliefStyle.NONE)
        reset_banner_button.set_tooltip_text(_("Remove custom banner"))
        reset_banner_button.connect("clicked", self.on_custom_image_reset_clicked, "banner")
        banner_box.pack_start(reset_banner_button, False, False, 0)

        self.icon_button = Gtk.Button()
        self._set_image("icon")
        self.icon_button.connect("clicked", self.on_custom_image_select, "icon")
        banner_box.pack_start(self.icon_button, False, False, 0)

        reset_icon_button = Gtk.Button.new_from_icon_name("edit-clear", Gtk.IconSize.MENU)
        reset_icon_button.set_relief(Gtk.ReliefStyle.NONE)
        reset_icon_button.set_tooltip_text(_("Remove custom icon"))
        reset_icon_button.connect("clicked", self.on_custom_image_reset_clicked, "icon")
        banner_box.pack_start(reset_icon_button, False, False, 0)

        return banner_box

    def _get_year_box(self):
        box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)

        label = Label(_("Release year"))
        box.pack_start(label, False, False, 0)

        self.year_entry = NumberEntry()
        if self.game:
            self.year_entry.set_text(str(self.game.year or ""))
        box.pack_start(self.year_entry, True, True, 0)

        return box

    def _set_image(self, image_format):
        image = Gtk.Image()
        service_media = LutrisBanner() if image_format == "banner" else LutrisIcon()
        game_slug = self.game.slug if self.game else ""
        image.set_from_pixbuf(service_media.get_pixbuf_for_game(game_slug))
        if image_format == "banner":
            self.banner_button.set_image(image)
        else:
            self.icon_button.set_image(image)

    def _get_runner_dropdown(self):
        runner_liststore = self._get_runner_liststore()
        runner_dropdown = Gtk.ComboBox.new_with_model(runner_liststore)
        runner_dropdown.set_id_column(1)
        runner_index = 0
        if self.runner_name:
            for runner in runner_liststore:
                if self.runner_name == str(runner[1]):
                    break
                runner_index += 1
        self.runner_index = runner_index
        runner_dropdown.set_active(self.runner_index)
        runner_dropdown.connect("changed", self.on_runner_changed)
        cell = Gtk.CellRendererText()
        cell.props.ellipsize = Pango.EllipsizeMode.END
        runner_dropdown.pack_start(cell, True)
        runner_dropdown.add_attribute(cell, "text", 0)
        return runner_dropdown

    @staticmethod
    def _get_runner_liststore():
        """Build a ListStore with available runners."""
        runner_liststore = Gtk.ListStore(str, str)
        runner_liststore.append((_("Select a runner from the list"), ""))
        for runner in runners.get_installed():
            description = runner.description
            runner_liststore.append(("%s (%s)" % (runner.human_name, description), runner.name))
        return runner_liststore

    def on_slug_change_clicked(self, widget):
        if self.slug_entry.get_sensitive() is False:
            widget.set_label(_("Apply"))
            self.slug_entry.set_sensitive(True)
        else:
            self.change_game_slug()

    def on_slug_entry_activate(self, _widget):
        self.change_game_slug()

    def change_game_slug(self):
        self.slug = self.slug_entry.get_text()
        self.slug_entry.set_sensitive(False)
        self.slug_change_button.set_label(_("Change"))

    def on_move_clicked(self, _button):
        new_location = DirectoryDialog("Select new location for the game",
                                       default_path=self.game.directory, parent=self)
        if not new_location.folder or new_location.folder == self.game.directory:
            return
        move_dialog = dialogs.MoveDialog(self.game, new_location.folder)
        move_dialog.connect("game-moved", self.on_game_moved)
        move_dialog.move()

    def on_game_moved(self, dialog):
        """Show a notification when the game is moved"""
        new_directory = dialog.new_directory
        if new_directory:
            self.directory_entry.set_text(new_directory)
            send_notification("Finished moving game", "%s moved to %s" % (dialog.game, new_directory))
        else:
            send_notification("Failed to move game", "Lutris could not move %s" % dialog.game)

    def _build_game_tab(self):
        if self.game and self.runner_name:
            self.game.runner_name = self.runner_name
            if not self.game.runner or self.game.runner.name != self.runner_name:
                try:
                    self.game.runner = runners.import_runner(self.runner_name)()
                except runners.InvalidRunner:
                    pass
            self.game_box = GameBox(self.lutris_config, self.game)
            game_sw = self.build_scrolled_window(self.game_box)
        elif self.runner_name:
            game = Game(None)
            game.runner_name = self.runner_name
            self.game_box = GameBox(self.lutris_config, game)
            game_sw = self.build_scrolled_window(self.game_box)
        else:
            game_sw = Gtk.Label(label=self.no_runner_label)
        self._add_notebook_tab(game_sw, _("Game options"))

    def _build_runner_tab(self, _config_level):
        if self.runner_name:
            self.runner_box = RunnerBox(self.lutris_config, self.game)
            runner_sw = self.build_scrolled_window(self.runner_box)
        else:
            runner_sw = Gtk.Label(label=self.no_runner_label)
        self._add_notebook_tab(runner_sw, _("Runner options"))

    def _build_system_tab(self, _config_level):
        if not self.lutris_config:
            raise RuntimeError("Lutris config not loaded yet")
        self.system_box = SystemBox(self.lutris_config)
        self._add_notebook_tab(
            self.build_scrolled_window(self.system_box),
            _("System options")
        )

    def _add_notebook_tab(self, widget, label):
        self.notebook.append_page(widget, Gtk.Label(label=label))

    def build_action_area(self, button_callback):
        self.action_area.set_layout(Gtk.ButtonBoxStyle.EDGE)

        # Advanced settings checkbox
        checkbox = Gtk.CheckButton(label=_("Show advanced options"))
        if settings.read_setting("show_advanced_options") == "True":
            checkbox.set_active(True)
        checkbox.connect("toggled", self.on_show_advanced_options_toggled)
        self.action_area.pack_start(checkbox, False, False, 5)

        # Buttons
        hbox = Gtk.Box()
        cancel_button = Gtk.Button(label=_("Cancel"))
        cancel_button.connect("clicked", self.on_cancel_clicked)
        hbox.pack_start(cancel_button, True, True, 10)

        save_button = Gtk.Button(label=_("Save"))
        save_button.connect("clicked", button_callback)
        hbox.pack_start(save_button, True, True, 0)
        self.action_area.pack_start(hbox, True, True, 0)

    def on_show_advanced_options_toggled(self, checkbox):
        value = bool(checkbox.get_active())
        settings.write_setting("show_advanced_options", value)

        self._set_advanced_options_visible(value)

    def _set_advanced_options_visible(self, value):
        """Change visibility of advanced options across all config tabs."""
        widgets = self.system_box.get_children()
        if self.runner_name:
            widgets += self.runner_box.get_children()
        if self.game:
            widgets += self.game_box.get_children()

        for widget in widgets:
            if widget.get_style_context().has_class("advanced"):
                widget.set_visible(value)
                if value:
                    widget.set_no_show_all(not value)
                    widget.show_all()

    def on_runner_changed(self, widget):
        """Action called when runner drop down is changed."""
        new_runner_index = widget.get_active()
        if self.runner_index and new_runner_index != self.runner_index:
            dlg = QuestionDialog(
                {
                    "parent": self,
                    "question":
                    _("Are you sure you want to change the runner for this game ? "
                      "This will reset the full configuration for this game and "
                      "is not reversible."),
                    "title":
                    _("Confirm runner change"),
                }
            )

            if dlg.result == Gtk.ResponseType.YES:
                self.runner_index = new_runner_index
                self._switch_runner(widget)
            else:
                # Revert the dropdown menu to the previously selected runner
                widget.set_active(self.runner_index)
        else:
            self.runner_index = new_runner_index
            self._switch_runner(widget)

    def _switch_runner(self, widget):
        """Rebuilds the UI on runner change"""
        current_page = self.notebook.get_current_page()
        if self.runner_index == 0:
            logger.info("No runner selected, resetting configuration")
            self.runner_name = None
            self.lutris_config = None
        else:
            runner_name = widget.get_model()[self.runner_index][1]
            if runner_name == self.runner_name:
                logger.debug("Runner unchanged, not creating a new config")
                return
            logger.info("Creating new configuration with runner %s", runner_name)
            self.runner_name = runner_name
            self.lutris_config = LutrisConfig(runner_slug=self.runner_name, level="game")
        self._rebuild_tabs()
        self.notebook.set_current_page(current_page)

    def _rebuild_tabs(self):
        for i in range(self.notebook.get_n_pages(), 1, -1):
            self.notebook.remove_page(i - 1)
        self._build_game_tab()
        self._build_runner_tab("game")
        self._build_system_tab("game")
        self.show_all()

    def on_cancel_clicked(self, _widget=None, _event=None):
        """Dialog destroy callback."""
        if self.game:
            self.game.load_config()
        self.destroy()

    def is_valid(self):
        if not self.runner_name:
            ErrorDialog(_("Runner not provided"), parent=self)
            return False
        if not self.name_entry.get_text():
            ErrorDialog(_("Please fill in the name"), parent=self)
            return False
        if self.runner_name == "steam" and not self.lutris_config.game_config.get("appid"):
            ErrorDialog(_("Steam AppID not provided"), parent=self)
            return False
        invalid_fields = []
        runner_class = import_runner(self.runner_name)
        runner_instance = runner_class()
        for config in ["game", "runner"]:
            for k, v in getattr(self.lutris_config, config + "_config").items():
                option = runner_instance.find_option(config + "_options", k)
                if option is None:
                    continue
                validator = option.get("validator")
                if validator is not None:
                    try:
                        res = validator(v)
                        logger.debug("%s validated successfully: %s", k, res)
                    except Exception:
                        invalid_fields.append(option.get("label"))
        if invalid_fields:
            ErrorDialog(_("The following fields have invalid values: ") + ", ".join(invalid_fields), parent=self)
            return False
        return True

    def on_save(self, _button):
        """Save game info and destroy widget. Return True if success."""
        if not self.is_valid():
            logger.warning(_("Current configuration is not valid, ignoring save request"))
            return False
        name = self.name_entry.get_text()

        if not self.slug:
            self.slug = slugify(name)

        if not self.game:
            self.game = Game()

        year = None
        if self.year_entry.get_text():
            year = int(self.year_entry.get_text())

        if not self.lutris_config.game_config_id:
            self.lutris_config.game_config_id = make_game_config_id(self.slug)

        runner_class = runners.import_runner(self.runner_name)
        runner = runner_class(self.lutris_config)

        self.game.name = name
        self.game.slug = self.slug
        self.game.year = year
        self.game.game_config_id = self.lutris_config.game_config_id
        self.game.runner = runner
        self.game.runner_name = self.runner_name
        self.game.is_installed = True
        self.game.config = self.lutris_config
        self.game.save(save_config=True)
        self.destroy()
        self.saved = True
        return True

    def on_custom_image_select(self, _widget, image_type):
        dialog = Gtk.FileChooserNative.new(
            _("Please choose a custom image"),
            self,
            Gtk.FileChooserAction.OPEN,
            None,
            None,
        )

        image_filter = Gtk.FileFilter()
        image_filter.set_name(_("Images"))
        image_filter.add_pixbuf_formats()
        dialog.add_filter(image_filter)

        response = dialog.run()
        if response == Gtk.ResponseType.ACCEPT:
            image_path = dialog.get_filename()
            if image_type == "banner":
                self.game.has_custom_banner = True
                dest_path = os.path.join(settings.BANNER_PATH, "%s.jpg" % self.game.slug)
                size = BANNER_SIZE
                file_format = "jpeg"
            else:
                self.game.has_custom_icon = True
                dest_path = resources.get_icon_path(self.game.slug)
                size = ICON_SIZE
                file_format = "png"
            pixbuf = get_pixbuf(image_path, size)
            pixbuf.savev(dest_path, file_format, [], [])
            self._set_image(image_type)

            if image_type == "icon":
                system.update_desktop_icons()

        dialog.destroy()

    def on_custom_image_reset_clicked(self, _widget, image_type):
        if image_type == "banner":
            self.game.has_custom_banner = False
            dest_path = os.path.join(settings.BANNER_PATH, "%s.jpg" % self.game.slug)
        elif image_type == "icon":
            self.game.has_custom_icon = False
            dest_path = resources.get_icon_path(self.game.slug)
        else:
            raise ValueError("Unsupported image type %s" % image_type)
        if os.path.isfile(dest_path):
            os.remove(dest_path)
        self._set_image(image_type)