コード例 #1
0
class LogWindow(Gtk.ApplicationWindow):

    def __init__(self, title=None, buffer=None, application=None):
        super().__init__(icon_name="lutris", application=application)
        self.set_title(title)
        self.set_show_menubar(False)

        self.set_size_request(640, 480)
        self.buffer = buffer
        self.logtextview = LogTextView(self.buffer)

        self.vbox = Gtk.VBox(spacing=6)
        self.add(self.vbox)

        scrolledwindow = Gtk.ScrolledWindow(hexpand=True, vexpand=True, child=self.logtextview)
        self.vbox.pack_start(scrolledwindow, True, True, 0)

        self.search_entry = Gtk.SearchEntry()
        self.search_entry.props.placeholder_text = "Search..."
        self.search_entry.connect("stop-search", self.dettach_search_entry)
        self.search_entry.connect("search-changed", self.logtextview.find_first)
        self.search_entry.connect("next-match", self.logtextview.find_next)
        self.search_entry.connect("previous-match", self.logtextview.find_previous)

        self.connect("key-press-event", self.on_key_press_event)

        self.show_all()

    def on_key_press_event(self, widget, event):
        if event.keyval == Gdk.KEY_Escape:
            self.search_entry.emit("stop-search")
            return

        ctrl = (event.state & Gdk.ModifierType.CONTROL_MASK)
        if ctrl and event.keyval == Gdk.KEY_f:
            self.attach_search_entry()
            return

        shift = (event.state & Gdk.ModifierType.SHIFT_MASK)
        if event.keyval == Gdk.KEY_Return:
            if shift:
                self.search_entry.emit("previous-match")
            else:
                self.search_entry.emit("next-match")

    def attach_search_entry(self):
        if self.search_entry.props.parent is None:
            self.vbox.pack_start(self.search_entry, False, False, 0)
            self.show_all()
            self.search_entry.grab_focus()
            if len(self.search_entry.get_text()) > 0:
                self.logtextview.find_first(self.search_entry)

    def dettach_search_entry(self, searched_entry):
        if self.search_entry.props.parent is not None:
            self.logtextview.reset_search()
            self.vbox.remove(self.search_entry)
            # Replace to bottom of log
            adj = self.logtextview.get_vadjustment()
            self.logtextview.scroll_max = adj.get_lower()
コード例 #2
0
ファイル: sysinfo_box.py プロジェクト: xnick/lutris
    def __init__(self):
        super().__init__(visible=True)
        self.set_margin_top(40)
        self.set_margin_right(30)
        self.set_margin_left(30)

        sysinfo_frame = Gtk.Frame(visible=True)
        sysinfo_frame.set_size_request(550, 455)
        scrolled_window = Gtk.ScrolledWindow(visible=True)
        scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC,
                                   Gtk.PolicyType.AUTOMATIC)

        sysinfo_view = LogTextView(autoscroll=False)
        sysinfo_view.set_cursor_visible(False)
        scrolled_window.add(sysinfo_view)
        sysinfo_frame.add(scrolled_window)
        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 to clipboard"), visible=True)
        button_copy.connect("clicked", self._copy_text)
        sysinfo_label = Gtk.Label(visible=True)
        sysinfo_label.set_markup("<b>System information</b>")
        self.put(sysinfo_label, 60, 0)
        self.put(sysinfo_frame, 60, 24)
        self.put(button_copy, 60, 486)
コード例 #3
0
    def __init__(self, title=None, buffer=None, application=None):
        super().__init__(icon_name="lutris", application=application)
        self.set_title(title)
        self.set_show_menubar(False)

        self.set_size_request(640, 480)
        self.buffer = buffer
        self.logtextview = LogTextView(self.buffer)

        self.vbox = Gtk.VBox(spacing=6)
        self.add(self.vbox)

        scrolledwindow = Gtk.ScrolledWindow(hexpand=True,
                                            vexpand=True,
                                            child=self.logtextview)
        self.vbox.pack_start(scrolledwindow, True, True, 0)

        self.search_entry = Gtk.SearchEntry()
        self.search_entry.props.placeholder_text = _("Search...")
        self.search_entry.connect("stop-search", self.dettach_search_entry)
        self.search_entry.connect("search-changed",
                                  self.logtextview.find_first)
        self.search_entry.connect("next-match", self.logtextview.find_next)
        self.search_entry.connect("previous-match",
                                  self.logtextview.find_previous)

        self.connect("key-press-event", self.on_key_press_event)

        self.show_all()
コード例 #4
0
 def attach_logger(self, command):
     """Creates a TextBuffer and attach it to a command"""
     self.log_buffer = Gtk.TextBuffer()
     command.set_log_buffer(self.log_buffer)
     self.log_textview = LogTextView(self.log_buffer)
     scrolledwindow = Gtk.ScrolledWindow(hexpand=True, vexpand=True, child=self.log_textview)
     scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
     self.widget_box.pack_end(scrolledwindow, True, True, 10)
     scrolledwindow.show()
     self.log_textview.show()
コード例 #5
0
    def __init__(self, title=None, buffer=None, application=None):
        super().__init__(icon_name="lutris", application=application)
        self.set_title(title)
        self.set_show_menubar(False)

        self.set_size_request(640, 480)
        self.buffer = buffer
        self.logtextview = LogTextView(self.buffer)

        self.vbox = Gtk.VBox(spacing=6)
        self.add(self.vbox)

        scrolledwindow = Gtk.ScrolledWindow(
            hexpand=True, vexpand=True, child=self.logtextview
        )
        self.vbox.pack_start(scrolledwindow, True, True, 0)

        self.search_entry = Gtk.SearchEntry()
        self.search_entry.props.placeholder_text = "Search..."
        self.search_entry.connect("stop-search", self.dettach_search_entry)
        self.search_entry.connect("search-changed", self.logtextview.find_first)
        self.search_entry.connect("next-match", self.logtextview.find_next)
        self.search_entry.connect("previous-match", self.logtextview.find_previous)

        self.connect("key-press-event", self.on_key_press_event)

        self.show_all()
コード例 #6
0
ファイル: log.py プロジェクト: zsh-754207424/lutris
    def __init__(self, title=None, buffer=None, application=None):
        super().__init__()
        ui_filename = os.path.join(datapath.get(), "ui/log-window.ui")
        builder = Gtk.Builder()
        builder.add_from_file(ui_filename)
        builder.connect_signals(self)
        window = builder.get_object("log_window")
        window.set_title(title)
        self.title = title

        self.buffer = buffer
        self.logtextview = LogTextView(self.buffer)

        scrolled_window = builder.get_object("scrolled_window")
        scrolled_window.add(self.logtextview)

        self.search_entry = builder.get_object("search_entry")
        self.search_entry.connect("search-changed",
                                  self.logtextview.find_first)
        self.search_entry.connect("next-match", self.logtextview.find_next)
        self.search_entry.connect("previous-match",
                                  self.logtextview.find_previous)

        save_button = builder.get_object("save_button")
        save_button.connect("clicked", self.on_save_clicked)

        window.connect("key-press-event", self.on_key_press_event)
        window.show_all()
コード例 #7
0
    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"))
コード例 #8
0
 def attach_logger(self, command):
     """Creates a TextBuffer and attach it to a command"""
     self.log_buffer = Gtk.TextBuffer()
     command.set_log_buffer(self.log_buffer)
     self.log_textview = LogTextView(self.log_buffer)
     scrolledwindow = Gtk.ScrolledWindow(
         hexpand=True, vexpand=True, child=self.log_textview
     )
     scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
     self.widget_box.pack_end(scrolledwindow, True, True, 10)
     scrolledwindow.show()
     self.log_textview.show()
コード例 #9
0
ファイル: log.py プロジェクト: Patryk-Jaroszczyk/lutris
    def __init__(self, title=None, buffer=None, parent=None):
        # XXX Setting the parent attribute makes the log window stick to the
        # main window, while this doesn't happen with other types of dialogs.
        super().__init__(title, None, 0, ("_OK", Gtk.ResponseType.OK))
        self.set_size_request(640, 480)
        self.grid = Gtk.Grid()
        self.buffer = buffer
        self.logtextview = LogTextView(self.buffer)

        scrolledwindow = Gtk.ScrolledWindow(hexpand=True,
                                            vexpand=True,
                                            child=self.logtextview)
        self.vbox.add(scrolledwindow)
        self.show_all()
コード例 #10
0
ファイル: log.py プロジェクト: yurikoles/lutris
    def __init__(self, title=None, buffer=None, application=None):
        super().__init__(icon_name="lutris", application=application)
        self.set_title(title)
        self.set_show_menubar(False)

        self.set_size_request(640, 480)
        self.buffer = buffer
        self.logtextview = LogTextView(self.buffer)

        scrolledwindow = Gtk.ScrolledWindow(
            hexpand=True, vexpand=True, child=self.logtextview
        )

        self.add(scrolledwindow)
        self.show_all()
コード例 #11
0
    def __init__(self, code, name, parent):
        Gtk.Dialog.__init__(self, "Install script for {}".format(name), parent=parent)
        self.set_size_request(500, 350)
        self.set_border_width(0)

        self.scrolled_window = Gtk.ScrolledWindow()
        self.scrolled_window.set_hexpand(True)
        self.scrolled_window.set_vexpand(True)

        source_buffer = Gtk.TextBuffer()
        source_buffer.set_text(code)

        source_box = LogTextView(source_buffer, autoscroll=False)

        self.get_content_area().add(self.scrolled_window)
        self.scrolled_window.add(source_box)

        close_button = Gtk.Button("OK")
        close_button.connect("clicked", self.on_close)
        self.get_content_area().add(close_button)

        self.show_all()
コード例 #12
0
ファイル: installerwindow.py プロジェクト: ruankranz/lutris
class InstallerWindow(BaseApplicationWindow):
    """GUI for the install process."""
    def __init__(
        self,
        game_slug=None,
        installer_file=None,
        revision=None,
        parent=None,
        application=None,
    ):
        super().__init__(application=application)

        self.download_progress = None
        self.install_in_progress = False
        self.interpreter = None
        self.selected_directory = None  # Latest directory chosen by user
        self.parent = parent
        self.game_slug = game_slug
        self.installer_file = installer_file
        self.revision = revision

        self.log_buffer = None
        self.log_textview = None

        self.title_label = InstallerLabel()
        self.vbox.add(self.title_label)

        self.status_label = InstallerLabel()
        self.status_label.set_max_width_chars(80)
        self.status_label.set_property("wrap", True)
        self.status_label.set_selectable(True)
        self.vbox.add(self.status_label)

        self.widget_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.vbox.pack_start(self.widget_box, True, True, 0)

        self.location_entry = None

        self.vbox.add(Gtk.HSeparator())

        self.action_buttons = Gtk.Box(spacing=6)
        action_buttons_alignment = Gtk.Alignment.new(1, 0, 0, 0)
        action_buttons_alignment.add(self.action_buttons)
        self.vbox.pack_start(action_buttons_alignment, False, True, 0)

        self.cancel_button = Gtk.Button.new_with_mnemonic("C_ancel")
        self.cancel_button.set_tooltip_text(
            "Abort and revert the installation")
        self.cancel_button.connect("clicked", self.cancel_installation)
        self.action_buttons.add(self.cancel_button)

        self.eject_button = self.add_button("_Eject", self.on_eject_clicked)
        self.source_button = self.add_button("_View source",
                                             self.on_source_clicked)
        self.install_button = self.add_button("_Install",
                                              self.on_install_clicked)
        self.continue_button = self.add_button("_Continue")
        self.play_button = self.add_button("_Launch", self.launch_game)
        self.close_button = self.add_button("_Close", self.on_destroy)

        self.continue_handler = None

        # check if installer is local or online
        if system.path_exists(self.installer_file):
            self.get_scripts(local_script=True)
        else:
            self.title_label.set_markup("Waiting for response from %s" %
                                        (settings.SITE_URL))
            self.add_spinner()
            self.widget_box.show()
            self.title_label.show()
            self.get_scripts(local_script=False)

        self.present()

    def add_button(self, label, handler=None):
        button = Gtk.Button.new_with_mnemonic(label)
        if handler:
            button.connect("clicked", handler)
        self.action_buttons.add(button)
        return button

    def get_scripts(self, local_script=False):
        if local_script:
            self.on_scripts_obtained(
                interpreter.read_script(self.installer_file))
        else:
            jobs.AsyncCall(
                interpreter.fetch_script,
                self.on_scripts_obtained,
                self.game_slug,
                self.revision,
            )

    def on_scripts_obtained(self, scripts, _error=None):
        if not scripts:
            self.destroy()
            self.run_no_installer_dialog()
            return

        if not isinstance(scripts, list):
            scripts = [scripts]
        self.clean_widgets()
        self.scripts = scripts
        self.show_all()
        self.close_button.hide()
        self.play_button.hide()
        self.install_button.hide()
        self.source_button.hide()
        self.eject_button.hide()
        self.continue_button.hide()
        self.install_in_progress = True

        self.choose_installer()

    def run_no_installer_dialog(self):
        """Open dialog for 'no script available' situation."""
        dlg = NoInstallerDialog(self)
        if dlg.result == dlg.MANUAL_CONF:
            game_data = pga.get_game_by_field(self.game_slug, "slug")

            if game_data and "slug" in game_data:
                # Game data already exist locally.
                game = Game(game_data["id"])
            else:
                # Try to load game data from remote.
                games = api.get_api_games([self.game_slug])

                if games and len(games) >= 1:
                    remote_game = games[0]
                    game_data = {
                        "name": remote_game["name"],
                        "slug": remote_game["slug"],
                        "year": remote_game["year"],
                        "updated": remote_game["updated"],
                        "steamid": remote_game["steamid"],
                    }
                    game = Game(pga.add_game(**game_data))
                else:
                    game = None
            AddGameDialog(self.parent, game=game)
        elif dlg.result == dlg.NEW_INSTALLER:
            webbrowser.open(settings.GAME_URL % self.game_slug)

    def validate_scripts(self):
        """Auto-fixes some script aspects and checks for mandatory fields"""
        for script in self.scripts:
            for item in ["description", "notes"]:
                script[item] = script.get(item) or ""
            for item in ["name", "runner", "version"]:
                if item not in script:
                    logger.error("Invalid script: %s", script)
                    raise ScriptingError(
                        'Missing field "%s" in install script' % item)

    def choose_installer(self):
        """Stage where we choose an install script."""
        self.validate_scripts()
        base_script = self.scripts[0]
        self.title_label.set_markup("<b>Install %s</b>" %
                                    escape_gtk_label(base_script["name"]))
        installer_picker = InstallerPicker(self.scripts)
        installer_picker.connect("installer-selected",
                                 self.on_installer_selected)
        scrolledwindow = Gtk.ScrolledWindow(hexpand=True,
                                            vexpand=True,
                                            child=installer_picker)
        scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self.widget_box.pack_end(scrolledwindow, True, True, 10)
        scrolledwindow.show()

    def prepare_install(self, script_slug):
        install_script = None
        for script in self.scripts:
            if script["slug"] == script_slug:
                install_script = script
        if not install_script:
            raise ValueError("Could not find script %s" % script_slug)
        try:
            self.interpreter = interpreter.ScriptInterpreter(
                install_script, self)
        except MissingGameDependency as ex:
            dlg = QuestionDialog({
                "question":
                "This game requires %s. Do you want to install it?" % ex.slug,
                "title":
                "Missing dependency",
            })
            if dlg.result == Gtk.ResponseType.YES:
                InstallerWindow(
                    game_slug=ex.slug,
                    parent=self.parent,
                    application=self.application,
                )
            self.destroy()
            return

        self.title_label.set_markup(u"<b>Installing {}</b>".format(
            escape_gtk_label(self.interpreter.game_name)))
        self.select_install_folder()

    def select_install_folder(self):
        """Stage where we select the install directory."""
        if self.interpreter.creates_game_folder:
            self.set_message("Select installation directory")
            default_path = self.interpreter.get_default_target()
            self.set_path_chooser(self.on_target_changed, "folder",
                                  default_path)

        else:
            self.set_message("Click install to continue")
        if self.continue_handler:
            self.continue_button.disconnect(self.continue_handler)
        self.continue_button.hide()
        self.source_button.show()
        self.install_button.grab_focus()
        self.install_button.show()

    def on_installer_selected(self, widget, installer_slug):
        self.clean_widgets()
        self.prepare_install(installer_slug)

    def on_target_changed(self, text_entry, _data):
        """Set the installation target for the game."""
        self.interpreter.target_path = os.path.expanduser(
            text_entry.get_text())

    def on_install_clicked(self, button):
        """Let the interpreter take charge of the next stages."""
        button.hide()
        self.source_button.hide()
        self.interpreter.check_runner_install()

    def ask_user_for_file(self, message):
        self.clean_widgets()
        self.set_message(message)
        path = self.selected_directory or os.path.expanduser("~")
        self.set_path_chooser(self.continue_guard, "file", default_path=path)

    def continue_guard(self, _, action):
        """This is weird and needs to be explained."""
        path = os.path.expanduser(self.location_entry.get_text())
        if (action == Gtk.FileChooserAction.OPEN and os.path.isfile(path)) or (
                action == Gtk.FileChooserAction.SELECT_FOLDER
                and os.path.isdir(path)):
            self.continue_button.set_sensitive(True)
            self.continue_button.connect("clicked", self.on_file_selected)
            self.continue_button.grab_focus()
        else:
            self.continue_button.set_sensitive(False)

    def set_path_chooser(self,
                         callback_on_changed,
                         action=None,
                         default_path=None):
        """Display a file/folder chooser."""
        self.install_button.set_visible(False)
        self.continue_button.show()
        self.continue_button.set_sensitive(False)

        if action == "file":
            title = "Select file"
            action = Gtk.FileChooserAction.OPEN
            enable_warnings = False
        elif action == "folder":
            title = "Select folder"
            action = Gtk.FileChooserAction.SELECT_FOLDER
            enable_warnings = True
        else:
            raise ValueError("Invalid action %s", action)

        if self.location_entry:
            self.location_entry.destroy()
        self.location_entry = FileChooserEntry(
            title,
            action,
            path=default_path,
            warn_if_non_empty=enable_warnings,
            warn_if_ntfs=enable_warnings)
        self.location_entry.entry.connect("changed", callback_on_changed,
                                          action)
        self.widget_box.pack_start(self.location_entry, False, False, 0)

    def on_file_selected(self, widget):
        file_path = os.path.expanduser(self.location_entry.get_text())
        if os.path.isfile(file_path):
            self.selected_directory = os.path.dirname(file_path)
        else:
            logger.warning("%s is not a file", file_path)
            return
        self.interpreter.file_selected(file_path)

    def start_download(self,
                       file_uri,
                       dest_file,
                       callback=None,
                       data=None,
                       referer=None):
        self.clean_widgets()
        logger.debug("Downloading %s to %s", file_uri, dest_file)
        self.download_progress = DownloadProgressBox(
            {
                "url": file_uri,
                "dest": dest_file,
                "referer": referer
            },
            cancelable=True)
        self.download_progress.cancel_button.hide()
        self.download_progress.connect("complete", self.on_download_complete,
                                       callback, data)
        self.widget_box.pack_start(self.download_progress, False, False, 10)
        self.download_progress.show()
        self.download_progress.start()
        self.interpreter.abort_current_task = self.download_progress.cancel

    def on_download_complete(self,
                             _widget,
                             _data,
                             callback=None,
                             callback_data=None):
        """Action called on a completed download."""
        if callback:
            try:
                callback_data = callback_data or {}
                callback(**callback_data)
            except Exception as ex:  # pylint: disable:broad-except
                raise ScriptingError(str(ex))

        self.interpreter.abort_current_task = None
        self.interpreter.iter_game_files()

    def ask_for_disc(self, message, callback, requires):
        """Ask the user to do insert a CD-ROM."""
        time.sleep(0.3)
        self.clean_widgets()
        label = InstallerLabel(message)
        label.show()
        self.widget_box.add(label)

        buttons_box = Gtk.Box()
        buttons_box.show()
        buttons_box.set_margin_top(40)
        buttons_box.set_margin_bottom(40)
        self.widget_box.add(buttons_box)

        autodetect_button = Gtk.Button(label="Autodetect")
        autodetect_button.connect("clicked", callback, requires)
        autodetect_button.grab_focus()
        autodetect_button.show()
        buttons_box.pack_start(autodetect_button, True, True, 40)

        browse_button = Gtk.Button(label="Browse…")
        callback_data = {"callback": callback, "requires": requires}
        browse_button.connect("clicked", self.on_browse_clicked, callback_data)
        browse_button.show()
        buttons_box.pack_start(browse_button, True, True, 40)

    def on_browse_clicked(self, widget, callback_data):
        dialog = DirectoryDialog("Select the folder where the disc is mounted",
                                 parent=self)
        folder = dialog.folder
        callback = callback_data["callback"]
        requires = callback_data["requires"]
        callback(widget, requires, folder)

    def on_eject_clicked(self, widget, data=None):
        self.interpreter.eject_wine_disc()

    def input_menu(self, alias, options, preselect, has_entry, callback):
        """Display an input request as a dropdown menu with options."""
        time.sleep(0.3)
        self.clean_widgets()

        model = Gtk.ListStore(str, str)
        for option in options:
            key, label = option.popitem()
            model.append([key, label])
        combobox = Gtk.ComboBox.new_with_model(model)
        renderer_text = Gtk.CellRendererText()
        combobox.pack_start(renderer_text, True)
        combobox.add_attribute(renderer_text, "text", 1)
        combobox.set_id_column(0)
        combobox.set_active_id(preselect)
        combobox.set_halign(Gtk.Align.CENTER)
        self.widget_box.pack_start(combobox, True, False, 100)

        combobox.connect("changed", self.on_input_menu_changed)
        combobox.show()
        self.continue_handler = self.continue_button.connect(
            "clicked", callback, alias, combobox)
        self.continue_button.grab_focus()
        self.continue_button.show()

        self.on_input_menu_changed(combobox)

    def on_input_menu_changed(self, widget):
        """Enable continue button if a non-empty choice is selected"""
        self.continue_button.set_sensitive(bool(widget.get_active_id()))

    def on_install_finished(self):
        self.clean_widgets()
        self.install_in_progress = False

        self.desktop_shortcut_box = Gtk.CheckButton("Create desktop shortcut")
        self.menu_shortcut_box = Gtk.CheckButton("Create application menu "
                                                 "shortcut")
        self.widget_box.pack_start(self.desktop_shortcut_box, False, False, 5)
        self.widget_box.pack_start(self.menu_shortcut_box, False, False, 5)
        self.widget_box.show_all()

        if settings.read_setting("create_desktop_shortcut") == "True":
            self.desktop_shortcut_box.set_active(True)
        if settings.read_setting("create_menu_shortcut") == "True":
            self.menu_shortcut_box.set_active(True)

        self.connect("delete-event", self.create_shortcuts)

        self.eject_button.hide()
        self.cancel_button.hide()
        self.continue_button.hide()
        self.install_button.hide()
        self.play_button.show()
        self.close_button.grab_focus()
        self.close_button.show()
        if not self.is_active():
            self.set_urgency_hint(True)  # Blink in taskbar
            self.connect("focus-in-event", self.on_window_focus)

    def on_window_focus(self, widget, *args):
        self.set_urgency_hint(False)

    def on_install_error(self, message):
        self.set_status(message)
        self.clean_widgets()
        self.cancel_button.grab_focus()

    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.game_id))

    def on_destroy(self, _widget, _data=None):
        """destroy event handler"""
        if self.install_in_progress:
            abort_close = self.cancel_installation()
            if abort_close:
                return True
        else:
            if self.interpreter:
                self.interpreter.cleanup()
            self.destroy()

    def create_shortcuts(self, *args):
        """Create desktop and global menu shortcuts."""
        game_slug = self.interpreter.game_slug
        game_id = self.interpreter.game_id
        game_name = self.interpreter.game_name
        create_desktop_shortcut = self.desktop_shortcut_box.get_active()
        create_menu_shortcut = self.menu_shortcut_box.get_active()

        if create_desktop_shortcut:
            xdgshortcuts.create_launcher(game_slug,
                                         game_id,
                                         game_name,
                                         desktop=True)
        if create_menu_shortcut:
            xdgshortcuts.create_launcher(game_slug,
                                         game_id,
                                         game_name,
                                         menu=True)

        settings.write_setting("create_desktop_shortcut",
                               create_desktop_shortcut)
        settings.write_setting("create_menu_shortcut", create_menu_shortcut)

    def cancel_installation(self, widget=None):
        """Ask a confirmation before cancelling the install"""
        remove_checkbox = Gtk.CheckButton.new_with_label("Remove game files")
        if self.interpreter:
            remove_checkbox.set_active(self.interpreter.game_dir_created)
            remove_checkbox.show()
        confirm_cancel_dialog = QuestionDialog({
            "question": "Are you sure you want to cancel the installation?",
            "title": "Cancel installation?",
            "widgets": [remove_checkbox]
        })
        if confirm_cancel_dialog.result != Gtk.ResponseType.YES:
            logger.debug("User cancelled installation")
            return True
        if self.interpreter:
            self.interpreter.game_dir_created = remove_checkbox.get_active()
            self.interpreter.revert()
            self.interpreter.cleanup()
        self.destroy()

    def on_source_clicked(self, _button):
        InstallerSourceDialog(self.interpreter.script_pretty,
                              self.interpreter.game_name, self)

    def clean_widgets(self):
        """Cleanup before displaying the next stage."""
        for child_widget in self.widget_box.get_children():
            child_widget.destroy()

    def set_status(self, text):
        """Display a short status text."""
        self.status_label.set_text(text)

    def set_message(self, message):
        """Display a message."""
        label = InstallerLabel()
        label.set_markup("<b>%s</b>" % add_url_tags(message))
        label.show()
        self.widget_box.pack_start(label, False, False, 18)

    def add_spinner(self):
        """Display a wait icon."""
        self.clean_widgets()
        spinner = Gtk.Spinner()
        self.widget_box.pack_start(spinner, False, False, 18)
        spinner.show()
        spinner.start()

    def attach_logger(self, command):
        """Creates a TextBuffer and attach it to a command"""
        self.log_buffer = Gtk.TextBuffer()
        command.set_log_buffer(self.log_buffer)
        self.log_textview = LogTextView(self.log_buffer)
        scrolledwindow = Gtk.ScrolledWindow(hexpand=True,
                                            vexpand=True,
                                            child=self.log_textview)
        scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self.widget_box.pack_end(scrolledwindow, True, True, 10)
        scrolledwindow.show()
        self.log_textview.show()
コード例 #13
0
class LogWindow(Gtk.ApplicationWindow):
    def __init__(self, title=None, buffer=None, application=None):
        super().__init__(icon_name="lutris", application=application)
        self.set_title(title)
        self.set_show_menubar(False)

        self.set_size_request(640, 480)
        self.buffer = buffer
        self.logtextview = LogTextView(self.buffer)

        self.vbox = Gtk.VBox(spacing=6)
        self.add(self.vbox)

        scrolledwindow = Gtk.ScrolledWindow(
            hexpand=True, vexpand=True, child=self.logtextview
        )
        self.vbox.pack_start(scrolledwindow, True, True, 0)

        self.search_entry = Gtk.SearchEntry()
        self.search_entry.props.placeholder_text = "Search..."
        self.search_entry.connect("stop-search", self.dettach_search_entry)
        self.search_entry.connect("search-changed", self.logtextview.find_first)
        self.search_entry.connect("next-match", self.logtextview.find_next)
        self.search_entry.connect("previous-match", self.logtextview.find_previous)

        self.connect("key-press-event", self.on_key_press_event)

        self.show_all()

    def on_key_press_event(self, widget, event):
        if event.keyval == Gdk.KEY_Escape:
            self.search_entry.emit("stop-search")
            return

        ctrl = (event.state & Gdk.ModifierType.CONTROL_MASK)
        if ctrl and event.keyval == Gdk.KEY_f:
            self.attach_search_entry()
            return

        shift = (event.state & Gdk.ModifierType.SHIFT_MASK)
        if event.keyval == Gdk.KEY_Return:
            if shift:
                self.search_entry.emit("previous-match")
            else:
                self.search_entry.emit("next-match")

    def attach_search_entry(self):
        if self.search_entry.props.parent is None:
            self.vbox.pack_start(self.search_entry, False, False, 0)
            self.show_all()
            self.search_entry.grab_focus()
            if len(self.search_entry.get_text()) > 0:
                self.logtextview.find_first(self.search_entry)

    def dettach_search_entry(self, searched_entry):
        if self.search_entry.props.parent is not None:
            self.logtextview.reset_search()
            self.vbox.remove(self.search_entry)
            # Replace to bottom of log
            adj = self.logtextview.get_vadjustment()
            self.logtextview.scroll_max = adj.get_lower()
コード例 #14
0
class InstallerWindow(BaseApplicationWindow):  # pylint: disable=too-many-public-methods
    """GUI for the install process."""

    def __init__(
        self,
        installers,
        service=None,
        appid=None,
        application=None,
    ):
        super().__init__(application=application)
        self.set_default_size(540, 320)
        self.installers = installers
        self.service = service
        self.appid = appid
        self.install_in_progress = False
        self.interpreter = None

        self.log_buffer = None
        self.log_textview = None

        self._cancel_files_func = None

        self.title_label = InstallerLabel()
        self.title_label.set_selectable(False)
        self.vbox.add(self.title_label)

        self.status_label = InstallerLabel()
        self.status_label.set_max_width_chars(80)
        self.status_label.set_property("wrap", True)
        self.status_label.set_selectable(True)
        self.vbox.add(self.status_label)

        self.widget_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.vbox.pack_start(self.widget_box, True, True, 0)

        self.vbox.add(Gtk.HSeparator())

        button_box = Gtk.Box()
        self.cache_button = Gtk.Button(_("Cache"))
        self.cache_button.connect("clicked", self.on_cache_clicked)
        button_box.add(self.cache_button)

        self.action_buttons = Gtk.Box(spacing=6)
        action_buttons_alignment = Gtk.Alignment.new(1, 0, 0, 0)
        action_buttons_alignment.add(self.action_buttons)
        button_box.pack_end(action_buttons_alignment, True, True, 0)
        self.vbox.pack_start(button_box, False, True, 0)

        self.cancel_button = self.add_button(
            _("C_ancel"), self.cancel_installation, tooltip=_("Abort and revert the installation")
        )
        self.eject_button = self.add_button(_("_Eject"), self.on_eject_clicked)
        self.source_button = self.add_button(_("_View source"), self.on_source_clicked)
        self.install_button = self.add_button(_("_Install"), self.on_install_clicked)
        self.continue_button = self.add_button(_("_Continue"))
        self.play_button = self.add_button(_("_Launch"), self.launch_game)
        self.close_button = self.add_button(_("_Close"), self.on_destroy)

        self.continue_handler = None

        self.clean_widgets()
        self.show_all()
        self.close_button.hide()
        self.play_button.hide()
        self.install_button.hide()
        self.source_button.hide()
        self.eject_button.hide()
        self.continue_button.hide()
        self.install_in_progress = True
        self.widget_box.show()
        self.title_label.show()
        self.choose_installer()

        self.present()

    def add_button(self, label, handler=None, tooltip=None):
        """Add a button to the action buttons box"""
        button = Gtk.Button.new_with_mnemonic(label)
        if tooltip:
            button.set_tooltip_text(tooltip)
        if handler:
            button.connect("clicked", handler)
        self.action_buttons.add(button)
        return button

    def validate_scripts(self):
        """Auto-fixes some script aspects and checks for mandatory fields"""
        for script in self.installers:
            for item in ["description", "notes"]:
                script[item] = script.get(item) or ""
            for item in ["name", "runner", "version"]:
                if item not in script:
                    logger.error("Invalid script: %s", script)
                    raise ScriptingError('Missing field "%s" in install script' % item)

    def choose_installer(self):
        """Stage where we choose an install script."""
        self.validate_scripts()
        base_script = self.installers[0]
        self.title_label.set_markup(_("<b>Install %s</b>") % gtk_safe(base_script["name"]))
        installer_picker = InstallerPicker(self.installers)
        installer_picker.connect("installer-selected", self.on_installer_selected)
        scrolledwindow = Gtk.ScrolledWindow(
            hexpand=True,
            vexpand=True,
            child=installer_picker,
            visible=True
        )
        scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self.widget_box.pack_end(scrolledwindow, True, True, 10)

    def get_script_from_slug(self, script_slug):
        """Return a installer script from its slug, raise an error if one isn't found"""
        for script in self.installers:
            if script["slug"] == script_slug:
                return script

    def on_cache_clicked(self, _button):
        """Open the cache configuration dialog"""
        CacheConfigurationDialog()

    def on_installer_selected(self, _widget, installer_slug):
        """Sets the script interpreter to the correct script then proceed to
        install folder selection.

        If the installed game depends on another one and it's not installed,
        prompt the user to install it and quit this installer.
        """
        self.clean_widgets()
        try:

            self.interpreter = interpreter.ScriptInterpreter(
                self.get_script_from_slug(installer_slug),
                self
            )

        except MissingGameDependency as ex:
            dlg = QuestionDialog(
                {
                    "question": _("This game requires %s. Do you want to install it?") % ex.slug,
                    "title": _("Missing dependency"),
                }
            )
            if dlg.result == Gtk.ResponseType.YES:
                InstallerWindow(
                    installers=self.installers,
                    service=self.service,
                    appid=self.appid,
                    application=self.application,
                )
            self.destroy()
            return
        self.title_label.set_markup(_(u"<b>Installing {}</b>").format(gtk_safe(self.interpreter.installer.game_name)))
        self.select_install_folder()

    def select_install_folder(self):
        """Stage where we select the install directory."""
        if not self.interpreter.installer.creates_game_folder:
            self.on_install_clicked(self.install_button)
            return
        self.set_message(_("Select installation directory"))
        default_path = self.interpreter.get_default_target()
        self.set_install_destination(default_path)
        if self.continue_handler:
            self.continue_button.disconnect(self.continue_handler)
        self.continue_button.hide()
        self.source_button.show()
        self.install_button.grab_focus()
        self.install_button.show()
        # self.manual_button.hide()

    def on_target_changed(self, text_entry, _data=None):
        """Set the installation target for the game."""
        self.interpreter.target_path = os.path.expanduser(text_entry.get_text())

    def on_install_clicked(self, button):
        """Let the interpreter take charge of the next stages."""
        button.hide()
        self.source_button.hide()
        self.interpreter.connect("runners-installed", self.on_runners_ready)
        GLib.idle_add(self.interpreter.launch_install)

    def set_install_destination(self, default_path=None):
        """Display the destination chooser."""
        self.install_button.set_visible(False)
        self.continue_button.show()
        self.continue_button.set_sensitive(False)
        location_entry = FileChooserEntry(
            "Select folder",
            Gtk.FileChooserAction.SELECT_FOLDER,
            path=default_path,
            warn_if_non_empty=True,
            warn_if_ntfs=True
        )
        location_entry.entry.connect("changed", self.on_target_changed)
        self.widget_box.pack_start(location_entry, False, False, 0)

    def ask_for_disc(self, message, callback, requires):
        """Ask the user to do insert a CD-ROM."""
        self.clean_widgets()
        label = InstallerLabel(message)
        label.show()
        self.widget_box.add(label)

        buttons_box = Gtk.Box()
        buttons_box.show()
        buttons_box.set_margin_top(40)
        buttons_box.set_margin_bottom(40)
        self.widget_box.add(buttons_box)

        autodetect_button = Gtk.Button(label=_("Autodetect"))
        autodetect_button.connect("clicked", callback, requires)
        autodetect_button.grab_focus()
        autodetect_button.show()
        buttons_box.pack_start(autodetect_button, True, True, 40)

        browse_button = Gtk.Button(label=_("Browse…"))
        callback_data = {"callback": callback, "requires": requires}
        browse_button.connect("clicked", self.on_browse_clicked, callback_data)
        browse_button.show()
        buttons_box.pack_start(browse_button, True, True, 40)

    def on_browse_clicked(self, widget, callback_data):
        dialog = DirectoryDialog(_("Select the folder where the disc is mounted"), parent=self)
        folder = dialog.folder
        callback = callback_data["callback"]
        requires = callback_data["requires"]
        callback(widget, requires, folder)

    def on_eject_clicked(self, _widget, data=None):
        self.interpreter.eject_wine_disc()

    def input_menu(self, alias, options, preselect, has_entry, callback):
        """Display an input request as a dropdown menu with options."""
        self.clean_widgets()

        model = Gtk.ListStore(str, str)
        for option in options:
            key, label = option.popitem()
            model.append([key, label])
        combobox = Gtk.ComboBox.new_with_model(model)
        renderer_text = Gtk.CellRendererText()
        combobox.pack_start(renderer_text, True)
        combobox.add_attribute(renderer_text, "text", 1)
        combobox.set_id_column(0)
        combobox.set_active_id(preselect)
        combobox.set_halign(Gtk.Align.CENTER)
        self.widget_box.pack_start(combobox, True, False, 100)

        combobox.connect("changed", self.on_input_menu_changed)
        combobox.show()
        if self.continue_handler:
            self.continue_button.disconnect(self.continue_handler)
        self.continue_handler = self.continue_button.connect("clicked", callback, alias, combobox)
        self.continue_button.grab_focus()
        self.continue_button.show()
        self.on_input_menu_changed(combobox)

    def on_input_menu_changed(self, widget):
        """Enable continue button if a non-empty choice is selected"""
        self.continue_button.set_sensitive(bool(widget.get_active_id()))

    def on_runners_ready(self, _widget=None):
        """The runners are ready, proceed with file selection"""
        if self.interpreter.extras is None:
            extras = self.interpreter.get_extras()
            if extras:
                self.show_extras(extras)
                return
        try:
            self.interpreter.installer.prepare_game_files()
        except UnavailableGame as ex:
            raise ScriptingError(str(ex))

        if not self.interpreter.installer.files:
            logger.debug("Installer doesn't require files")
            self.interpreter.launch_installer_commands()
            return
        self.show_installer_files_screen()

    def show_installer_files_screen(self):
        """Show installer screen with the file picker / downloader"""
        self.clean_widgets()
        self.set_status(_("Please review the files needed for the installation then click 'Continue'"))
        installer_files_box = InstallerFilesBox(self.interpreter.installer, self)
        installer_files_box.connect("files-available", self.on_files_available)
        installer_files_box.connect("files-ready", self.on_files_ready)
        self._cancel_files_func = installer_files_box.stop_all
        scrolledwindow = Gtk.ScrolledWindow(
            hexpand=True,
            vexpand=True,
            child=installer_files_box,
            visible=True
        )
        scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self.widget_box.pack_end(scrolledwindow, True, True, 10)

        self.continue_button.show()
        self.continue_button.set_sensitive(installer_files_box.is_ready)
        if self.continue_handler:
            self.continue_button.disconnect(self.continue_handler)
        self.continue_handler = self.continue_button.connect(
            "clicked", self.on_files_confirmed, installer_files_box
        )

    def get_extra_label(self, extra):
        """Return a label for the extras picker"""
        label = extra["name"]
        _infos = []
        if extra.get("total_size"):
            _infos.append(human_size(extra["total_size"]))
        if extra.get("type"):
            _infos.append(extra["type"])
        if _infos:
            label += " (%s)" % ", ".join(_infos)
        return label

    def show_extras(self, extras):
        """Show installer screen with the extras picker"""
        self.clean_widgets()
        extra_liststore = Gtk.ListStore(
            bool,   # is selected?
            str,  # id
            str,  # label
        )
        for extra in extras:
            extra_liststore.append((False, extra["id"], self.get_extra_label(extra)))

        treeview = Gtk.TreeView(extra_liststore)
        treeview.set_headers_visible(False)
        renderer_toggle = Gtk.CellRendererToggle()
        renderer_toggle.connect("toggled", self.on_extra_toggled, extra_liststore)
        renderer_text = Gtk.CellRendererText()

        installed_column = Gtk.TreeViewColumn(None, renderer_toggle, active=0)
        treeview.append_column(installed_column)

        label_column = Gtk.TreeViewColumn(None, renderer_text)
        label_column.add_attribute(renderer_text, "text", 2)
        label_column.set_property("min-width", 80)
        treeview.append_column(label_column)

        scrolledwindow = Gtk.ScrolledWindow(
            hexpand=True,
            vexpand=True,
            child=treeview,
            visible=True
        )
        scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        scrolledwindow.show_all()
        self.widget_box.pack_end(scrolledwindow, True, True, 10)
        self.continue_button.show()
        self.continue_button.set_sensitive(True)
        if self.continue_handler:
            self.continue_button.disconnect(self.continue_handler)
        self.continue_handler = self.continue_button.connect("clicked", self.on_extras_confirmed, extra_liststore)

    def on_extra_toggled(self, _widget, path, store):
        row = store[path]
        row[0] = not row[0]

    def on_extras_confirmed(self, _button, extra_store):
        """Resume install when user has selected extras to download"""
        selected_extras = []
        for extra in extra_store:
            if extra[0]:
                selected_extras.append(extra[1])
        self.interpreter.extras = selected_extras
        GLib.idle_add(self.on_runners_ready)

    def on_files_ready(self, _widget, files_ready):
        """Toggle state of continue button based on ready state"""
        logger.debug("Files are ready: %s", files_ready)
        self.continue_button.set_sensitive(files_ready)

    def on_files_confirmed(self, _button, file_box):
        """Call this when the user confirms the install files
        This will start the downloads.
        """
        self.set_status("")
        self.continue_button.set_sensitive(False)
        try:
            file_box.start_all()
            self.continue_button.disconnect(self.continue_handler)
        except PermissionError as ex:
            self.continue_button.set_sensitive(True)
            raise ScriptingError("Unable to get files: %s" % ex)

    def on_files_available(self, widget):
        """All files are available, continue the install"""
        logger.info("All files are available, continuing install")
        self._cancel_files_func = None
        self.continue_button.hide()
        self.interpreter.game_files = widget.get_game_files()
        self.clean_widgets()
        self.interpreter.launch_installer_commands()

    def on_install_finished(self):
        self.clean_widgets()
        self.install_in_progress = False

        desktop_shortcut_button = Gtk.Button(_("Create desktop shortcut"), visible=True)
        desktop_shortcut_button.connect("clicked", self.on_create_desktop_shortcut_clicked)
        self.widget_box.pack_start(desktop_shortcut_button, False, False, 5)

        menu_shortcut_button = Gtk.Button(_("Create application menu shortcut"), visible=True)
        menu_shortcut_button.connect("clicked", self.on_create_menu_shortcut_clicked)
        self.widget_box.pack_start(menu_shortcut_button, False, False, 5)

        self.widget_box.show()

        self.eject_button.hide()
        self.cancel_button.hide()
        self.continue_button.hide()
        self.install_button.hide()

        self.play_button.show()
        self.close_button.grab_focus()
        self.close_button.show()
        if not self.is_active():
            self.set_urgency_hint(True)  # Blink in taskbar
            self.connect("focus-in-event", self.on_window_focus)

    def on_window_focus(self, _widget, *_args):
        """Remove urgency hint (flashing indicator) when window receives focus"""
        self.set_urgency_hint(False)

    def on_install_error(self, message):
        self.clean_widgets()
        self.set_status(message)
        self.cancel_button.grab_focus()

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

    def on_destroy(self, _widget, _data=None):
        """destroy event handler"""
        if self.install_in_progress:
            abort_close = self.cancel_installation()
            if abort_close:
                return True
        else:
            if self.interpreter:
                self.interpreter.cleanup()
            self.destroy()

    def on_create_desktop_shortcut_clicked(self, _widget):
        self.create_shortcut(desktop=True)

    def on_create_menu_shortcut_clicked(self, _widget):
        self.create_shortcut()

    def create_shortcut(self, desktop=False):
        """Create desktop or global menu shortcuts."""
        game_slug = self.interpreter.installer.game_slug
        game_id = self.interpreter.installer.game_id
        game_name = self.interpreter.installer.game_name

        if desktop:
            xdgshortcuts.create_launcher(game_slug, game_id, game_name, desktop=True)
        else:
            xdgshortcuts.create_launcher(game_slug, game_id, game_name, menu=True)

    def cancel_installation(self, _widget=None):
        """Ask a confirmation before cancelling the install"""
        remove_checkbox = Gtk.CheckButton.new_with_label(_("Remove game files"))
        if self.interpreter:
            remove_checkbox.set_active(self.interpreter.game_dir_created)
            remove_checkbox.show()
        confirm_cancel_dialog = QuestionDialog(
            {
                "question": _("Are you sure you want to cancel the installation?"),
                "title": _("Cancel installation?"),
                "widgets": [remove_checkbox]
            }
        )
        if confirm_cancel_dialog.result != Gtk.ResponseType.YES:
            logger.debug("User aborted installation cancellation")
            return True
        if self._cancel_files_func:
            self._cancel_files_func()
        if self.interpreter:
            self.interpreter.revert()
            self.interpreter.cleanup()
        self.destroy()

    def on_source_clicked(self, _button):
        InstallerSourceDialog(
            self.interpreter.installer.script_pretty,
            self.interpreter.installer.game_name,
            self
        )

    def clean_widgets(self):
        """Cleanup before displaying the next stage."""
        for child_widget in self.widget_box.get_children():
            child_widget.destroy()

    def set_status(self, text):
        """Display a short status text."""
        self.status_label.set_text(text)

    def set_message(self, message):
        """Display a message."""
        label = InstallerLabel()
        label.set_markup("<b>%s</b>" % add_url_tags(message))
        label.show()
        self.widget_box.pack_start(label, False, False, 18)

    def add_spinner(self):
        """Show a spinner in the middle of the view"""
        self.clean_widgets()
        spinner = Gtk.Spinner()
        self.widget_box.pack_start(spinner, False, False, 18)
        spinner.show()
        spinner.start()

    def attach_logger(self, command):
        """Creates a TextBuffer and attach it to a command"""
        self.log_buffer = Gtk.TextBuffer()
        command.set_log_buffer(self.log_buffer)
        self.log_textview = LogTextView(self.log_buffer)
        scrolledwindow = Gtk.ScrolledWindow(hexpand=True, vexpand=True, child=self.log_textview)
        scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self.widget_box.pack_end(scrolledwindow, True, True, 10)
        scrolledwindow.show()
        self.log_textview.show()
コード例 #15
0
class InstallerWindow(BaseApplicationWindow):  # pylint: disable=too-many-public-methods
    """GUI for the install process."""
    def __init__(
        self,
        game_slug=None,
        installer_file=None,
        revision=None,
        parent=None,
        application=None,
    ):
        super().__init__(application=application)

        self.install_in_progress = False
        self.interpreter = None
        self.parent = parent
        self.game_slug = game_slug
        self.revision = revision
        self.desktop_shortcut_box = None
        self.menu_shortcut_box = None

        self.log_buffer = None
        self.log_textview = None

        self.title_label = InstallerLabel()
        self.title_label.set_selectable(False)
        self.vbox.add(self.title_label)

        self.status_label = InstallerLabel()
        self.status_label.set_max_width_chars(80)
        self.status_label.set_property("wrap", True)
        self.status_label.set_selectable(True)
        self.vbox.add(self.status_label)

        self.widget_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.vbox.pack_start(self.widget_box, True, True, 0)

        self.vbox.add(Gtk.HSeparator())

        self.action_buttons = Gtk.Box(spacing=6)
        action_buttons_alignment = Gtk.Alignment.new(1, 0, 0, 0)
        action_buttons_alignment.add(self.action_buttons)
        self.vbox.pack_start(action_buttons_alignment, False, True, 0)

        self.manual_button = self.add_button(_("Configure m_anually"),
                                             self.on_manual_clicked)
        self.cancel_button = self.add_button(
            _("C_ancel"),
            self.cancel_installation,
            tooltip=_("Abort and revert the installation"))
        self.eject_button = self.add_button(_("_Eject"), self.on_eject_clicked)
        self.source_button = self.add_button(_("_View source"),
                                             self.on_source_clicked)
        self.install_button = self.add_button(_("_Install"),
                                              self.on_install_clicked)
        self.continue_button = self.add_button(_("_Continue"))
        self.play_button = self.add_button(_("_Launch"), self.launch_game)
        self.close_button = self.add_button(_("_Close"), self.on_destroy)

        self.continue_handler = None

        # check if installer is local or online
        if system.path_exists(installer_file):
            self.on_scripts_obtained(interpreter.read_script(installer_file))
        else:
            self.title_label.set_markup(
                _("Waiting for response from %s") % (settings.SITE_URL))
            self.add_spinner()
            self.widget_box.show()
            self.title_label.show()
            jobs.AsyncCall(
                interpreter.fetch_script,
                self.on_scripts_obtained,
                self.game_slug,
                self.revision,
            )
        self.present()

    def add_button(self, label, handler=None, tooltip=None):
        """Add a button to the action buttons box"""
        button = Gtk.Button.new_with_mnemonic(label)
        if tooltip:
            button.set_tooltip_text(tooltip)
        if handler:
            button.connect("clicked", handler)

        self.action_buttons.add(button)
        return button

    def on_scripts_obtained(self, scripts, _error=None):
        """Continue the install process when the scripts are available"""
        if not scripts:
            self.destroy()
            self.run_no_installer_dialog()
            return

        if not isinstance(scripts, list):
            scripts = [scripts]
        self.clean_widgets()
        self.scripts = scripts
        self.show_all()
        self.close_button.hide()
        self.play_button.hide()
        self.install_button.hide()
        self.source_button.hide()
        self.eject_button.hide()
        self.continue_button.hide()
        self.install_in_progress = True

        self.choose_installer()

    def run_no_installer_dialog(self):
        """Open dialog for 'no script available' situation."""
        dlg = NoInstallerDialog(self)
        if dlg.result == dlg.MANUAL_CONF:
            self.manually_configure_game()
        elif dlg.result == dlg.NEW_INSTALLER:
            webbrowser.open(settings.GAME_URL % self.game_slug)

    def manually_configure_game(self):
        game_data = pga.get_game_by_field(self.game_slug, "slug")

        if game_data and "slug" in game_data:
            # Game data already exist locally.
            game = Game(game_data["id"])
        else:
            # Try to load game data from remote.
            games = api.get_api_games([self.game_slug])

            if games and len(games) >= 1:
                remote_game = games[0]
                game_data = {
                    "name": remote_game["name"],
                    "slug": remote_game["slug"],
                    "year": remote_game["year"],
                    "updated": remote_game["updated"],
                    "steamid": remote_game["steamid"],
                }
                game = Game(pga.add_game(**game_data))
            else:
                game = None
        AddGameDialog(self.parent, game=game)

    def on_manual_clicked(self, widget):
        self.destroy()
        self.manually_configure_game()

    def validate_scripts(self):
        """Auto-fixes some script aspects and checks for mandatory fields"""
        for script in self.scripts:
            for item in ["description", "notes"]:
                script[item] = script.get(item) or ""
            for item in ["name", "runner", "version"]:
                if item not in script:
                    logger.error("Invalid script: %s", script)
                    raise ScriptingError(
                        'Missing field "%s" in install script' % item)

    def choose_installer(self):
        """Stage where we choose an install script."""
        self.validate_scripts()
        base_script = self.scripts[0]
        self.title_label.set_markup(
            _("<b>Install %s</b>") % gtk_safe(base_script["name"]))
        installer_picker = InstallerPicker(self.scripts)
        installer_picker.connect("installer-selected",
                                 self.on_installer_selected)
        scrolledwindow = Gtk.ScrolledWindow(hexpand=True,
                                            vexpand=True,
                                            child=installer_picker,
                                            visible=True)
        scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self.widget_box.pack_end(scrolledwindow, True, True, 10)

    def get_script_from_slug(self, script_slug):
        """Return a installer script from its slug, raise an error if one isn't found"""
        install_script = None
        for script in self.scripts:
            if script["slug"] == script_slug:
                install_script = script
        if not install_script:
            raise ValueError("Could not find script %s" % script_slug)
        return install_script

    def on_installer_selected(self, _widget, installer_slug):
        """Sets the script interpreter to the correct script then proceed to
        install folder selection.

        If the installed game depends on another one and it's not installed,
        prompt the user to install it and quit this installer.
        """
        self.clean_widgets()
        try:
            self.interpreter = interpreter.ScriptInterpreter(
                self.get_script_from_slug(installer_slug), self)
        except MissingGameDependency as ex:
            dlg = QuestionDialog({
                "question":
                _("This game requires %s. Do you want to install it?") %
                ex.slug,
                "title":
                _("Missing dependency"),
            })
            if dlg.result == Gtk.ResponseType.YES:
                InstallerWindow(
                    game_slug=ex.slug,
                    parent=self.parent,
                    application=self.application,
                )
            self.destroy()
            return
        self.title_label.set_markup(
            _(u"<b>Installing {}</b>").format(
                gtk_safe(self.interpreter.installer.game_name)))
        self.select_install_folder()

    def select_install_folder(self):
        """Stage where we select the install directory."""
        if not self.interpreter.installer.creates_game_folder:
            self.on_install_clicked(self.install_button)
            return
        self.set_message(_("Select installation directory"))
        default_path = self.interpreter.get_default_target()
        self.set_install_destination(default_path)
        if self.continue_handler:
            self.continue_button.disconnect(self.continue_handler)
        self.continue_button.hide()
        self.source_button.show()
        self.install_button.grab_focus()
        self.install_button.show()
        self.manual_button.hide()

    def on_target_changed(self, text_entry, _data=None):
        """Set the installation target for the game."""
        self.interpreter.target_path = os.path.expanduser(
            text_entry.get_text())

    def on_install_clicked(self, button):
        """Let the interpreter take charge of the next stages."""
        button.hide()
        self.source_button.hide()
        self.interpreter.connect("runners-installed", self.on_runners_ready)
        self.interpreter.launch_install()

    def set_install_destination(self, default_path=None):
        """Display the destination chooser."""
        self.install_button.set_visible(False)
        self.continue_button.show()
        self.continue_button.set_sensitive(False)
        location_entry = FileChooserEntry("Select folder",
                                          Gtk.FileChooserAction.SELECT_FOLDER,
                                          default_path=default_path,
                                          path_type=PATH_TYPE.INSTALL_TO,
                                          warnings=FileChooserWarnings.NTFS
                                          | FileChooserWarnings.NON_EMPTY)
        location_entry.entry.connect("changed", self.on_target_changed)
        self.widget_box.pack_start(location_entry, False, False, 0)

    def ask_for_disc(self, message, callback, requires):
        """Ask the user to do insert a CD-ROM."""
        time.sleep(0.3)
        self.clean_widgets()
        label = InstallerLabel(message)
        label.show()
        self.widget_box.add(label)

        buttons_box = Gtk.Box()
        buttons_box.show()
        buttons_box.set_margin_top(40)
        buttons_box.set_margin_bottom(40)
        self.widget_box.add(buttons_box)

        autodetect_button = Gtk.Button(label=_("Autodetect"))
        autodetect_button.connect("clicked", callback, requires)
        autodetect_button.grab_focus()
        autodetect_button.show()
        buttons_box.pack_start(autodetect_button, True, True, 40)

        browse_button = Gtk.Button(label=_("Browse…"))
        callback_data = {"callback": callback, "requires": requires}
        browse_button.connect("clicked", self.on_browse_clicked, callback_data)
        browse_button.show()
        buttons_box.pack_start(browse_button, True, True, 40)

    def on_browse_clicked(self, widget, callback_data):
        dialog = DirectoryDialog(
            _("Select the folder where the disc is mounted"), parent=self)
        folder = dialog.folder
        callback = callback_data["callback"]
        requires = callback_data["requires"]
        callback(widget, requires, folder)

    def on_eject_clicked(self, _widget, data=None):
        self.interpreter.eject_wine_disc()

    def input_menu(self, alias, options, preselect, has_entry, callback):
        """Display an input request as a dropdown menu with options."""
        time.sleep(0.3)
        self.clean_widgets()

        model = Gtk.ListStore(str, str)
        for option in options:
            key, label = option.popitem()
            model.append([key, label])
        combobox = Gtk.ComboBox.new_with_model(model)
        renderer_text = Gtk.CellRendererText()
        combobox.pack_start(renderer_text, True)
        combobox.add_attribute(renderer_text, "text", 1)
        combobox.set_id_column(0)
        combobox.set_active_id(preselect)
        combobox.set_halign(Gtk.Align.CENTER)
        self.widget_box.pack_start(combobox, True, False, 100)

        combobox.connect("changed", self.on_input_menu_changed)
        combobox.show()
        self.continue_handler = self.continue_button.connect(
            "clicked", callback, alias, combobox)
        self.continue_button.grab_focus()
        self.continue_button.show()

        self.on_input_menu_changed(combobox)

    def on_input_menu_changed(self, widget):
        """Enable continue button if a non-empty choice is selected"""
        self.continue_button.set_sensitive(bool(widget.get_active_id()))

    def on_runners_ready(self, _widget):
        """The runners are ready, proceed with file selection"""
        self.clean_widgets()
        self.interpreter.installer.prepare_game_files()
        if not self.interpreter.installer.files:
            logger.debug("Installer doesn't require files")
            self.interpreter.launch_installer_commands()
            return

        self.set_status(
            "Please review the files needed for the installation then click 'Continue'"
        )
        installer_files_box = InstallerFilesBox(
            self.interpreter.installer.files, self)
        installer_files_box.connect("files-available", self.on_files_available)
        installer_files_box.connect("files-ready", self.on_files_ready)
        scrolledwindow = Gtk.ScrolledWindow(hexpand=True,
                                            vexpand=True,
                                            child=installer_files_box,
                                            visible=True)
        scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self.widget_box.pack_end(scrolledwindow, True, True, 10)

        self.continue_button.show()
        self.continue_button.set_sensitive(installer_files_box.is_ready)
        self.continue_handler = self.continue_button.connect(
            "clicked", self.on_files_confirmed, installer_files_box)

    def on_files_ready(self, _widget, is_ready):
        """Toggle state of continue button based on ready state"""
        logger.debug("Files are ready: %s", is_ready)
        self.continue_button.set_sensitive(is_ready)

    def on_files_confirmed(self, _button, file_box):
        """Call this when the user confirms the install files
        This will start the downloads.
        """
        self.continue_button.set_sensitive(False)
        self.continue_button.disconnect(self.continue_handler)
        file_box.start_all()

    def on_files_available(self, widget):
        """All files are available, continue the install"""
        logger.info("All files are available, continuing install")
        self.continue_button.hide()
        self.interpreter.game_files = widget.get_game_files()
        self.clean_widgets()
        self.interpreter.launch_installer_commands()

    def on_install_finished(self):
        self.clean_widgets()
        self.install_in_progress = False

        self.desktop_shortcut_box = Gtk.CheckButton(
            _("Create desktop shortcut"), visible=True)
        self.menu_shortcut_box = Gtk.CheckButton(
            _("Create application menu shortcut"), visible=True)
        self.widget_box.pack_start(self.desktop_shortcut_box, False, False, 5)
        self.widget_box.pack_start(self.menu_shortcut_box, False, False, 5)
        self.widget_box.show()

        if settings.read_setting("create_desktop_shortcut") == "True":
            self.desktop_shortcut_box.set_active(True)
        if settings.read_setting("create_menu_shortcut") == "True":
            self.menu_shortcut_box.set_active(True)

        self.connect("delete-event", self.create_shortcuts)

        self.eject_button.hide()
        self.cancel_button.hide()
        self.continue_button.hide()
        self.install_button.hide()
        self.play_button.show()
        self.close_button.grab_focus()
        self.close_button.show()
        if not self.is_active():
            self.set_urgency_hint(True)  # Blink in taskbar
            self.connect("focus-in-event", self.on_window_focus)

    def on_window_focus(self, _widget, *_args):
        """Remove urgency hint (flashing indicator) when window receives focus"""
        self.set_urgency_hint(False)

    def on_install_error(self, message):
        self.set_status(message)
        self.clean_widgets()
        self.cancel_button.grab_focus()

    def launch_game(self, widget, _data=None):
        """Launch a game after it's been installed."""
        widget.set_sensitive(False)
        self.on_destroy(widget)
        self.application.launch(Game(self.interpreter.installer.game_id))

    def on_destroy(self, _widget, _data=None):
        """destroy event handler"""
        if self.install_in_progress:
            abort_close = self.cancel_installation()
            if abort_close:
                return True
        else:
            if self.interpreter:
                self.interpreter.cleanup()
            self.destroy()

    def create_shortcuts(self, *args):
        """Create desktop and global menu shortcuts."""
        game_slug = self.interpreter.installer.game_slug
        game_id = self.interpreter.installer.game_id
        game_name = self.interpreter.installer.game_name
        create_desktop_shortcut = self.desktop_shortcut_box.get_active()
        create_menu_shortcut = self.menu_shortcut_box.get_active()

        if create_desktop_shortcut:
            xdgshortcuts.create_launcher(game_slug,
                                         game_id,
                                         game_name,
                                         desktop=True)
        if create_menu_shortcut:
            xdgshortcuts.create_launcher(game_slug,
                                         game_id,
                                         game_name,
                                         menu=True)

        settings.write_setting("create_desktop_shortcut",
                               create_desktop_shortcut)
        settings.write_setting("create_menu_shortcut", create_menu_shortcut)

    def cancel_installation(self, _widget=None):
        """Ask a confirmation before cancelling the install"""
        remove_checkbox = Gtk.CheckButton.new_with_label(
            _("Remove game files"))
        if self.interpreter:
            remove_checkbox.set_active(self.interpreter.game_dir_created)
            remove_checkbox.show()
        confirm_cancel_dialog = QuestionDialog({
            "question":
            _("Are you sure you want to cancel the installation?"),
            "title":
            _("Cancel installation?"),
            "widgets": [remove_checkbox]
        })
        if confirm_cancel_dialog.result != Gtk.ResponseType.YES:
            logger.debug("User cancelled installation")
            return True
        if self.interpreter:
            self.interpreter.revert()
            self.interpreter.cleanup()
        self.destroy()

    def on_source_clicked(self, _button):
        InstallerSourceDialog(self.interpreter.installer.script_pretty,
                              self.interpreter.installer.game_name, self)

    def clean_widgets(self):
        """Cleanup before displaying the next stage."""
        for child_widget in self.widget_box.get_children():
            child_widget.destroy()

    def set_status(self, text):
        """Display a short status text."""
        self.status_label.set_text(text)

    def set_message(self, message):
        """Display a message."""
        label = InstallerLabel()
        label.set_markup("<b>%s</b>" % add_url_tags(message))
        label.show()
        self.widget_box.pack_start(label, False, False, 18)

    def add_spinner(self):
        """Display a wait icon."""
        self.clean_widgets()
        spinner = Gtk.Spinner()
        self.widget_box.pack_start(spinner, False, False, 18)
        spinner.show()
        spinner.start()

    def attach_logger(self, command):
        """Creates a TextBuffer and attach it to a command"""
        self.log_buffer = Gtk.TextBuffer()
        command.set_log_buffer(self.log_buffer)
        self.log_textview = LogTextView(self.log_buffer)
        scrolledwindow = Gtk.ScrolledWindow(hexpand=True,
                                            vexpand=True,
                                            child=self.log_textview)
        scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self.widget_box.pack_end(scrolledwindow, True, True, 10)
        scrolledwindow.show()
        self.log_textview.show()
コード例 #16
0
class InstallerWindow(BaseApplicationWindow):
    """GUI for the install process."""
    def __init__(
            self,
            game_slug=None,
            installer_file=None,
            revision=None,
            parent=None,
            application=None,
    ):
        super().__init__(application=application)

        self.download_progress = None
        self.install_in_progress = False
        self.interpreter = None
        self.selected_directory = None  # Latest directory chosen by user
        self.parent = parent
        self.game_slug = game_slug
        self.installer_file = installer_file
        self.revision = revision

        self.log_buffer = None
        self.log_textview = None

        self.title_label = Gtk.Label()
        self.vbox.add(self.title_label)

        self.status_label = Gtk.Label()
        self.status_label.set_max_width_chars(80)
        self.status_label.set_property("wrap", True)
        self.status_label.set_selectable(True)
        self.vbox.add(self.status_label)

        self.widget_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.vbox.pack_start(self.widget_box, True, True, 0)

        self.location_entry = None

        self.vbox.add(Gtk.HSeparator())

        self.action_buttons = Gtk.Box(spacing=6)
        action_buttons_alignment = Gtk.Alignment.new(1, 0, 0, 0)
        action_buttons_alignment.add(self.action_buttons)
        self.vbox.pack_start(action_buttons_alignment, False, True, 0)

        self.cancel_button = Gtk.Button.new_with_mnemonic("C_ancel")
        self.cancel_button.set_tooltip_text("Abort and revert the " "installation")
        self.cancel_button.connect("clicked", self.cancel_installation)
        self.action_buttons.add(self.cancel_button)

        self.eject_button = self.add_button("_Eject", self.on_eject_clicked)
        self.source_button = self.add_button("_View source", self.on_source_clicked)
        self.install_button = self.add_button("_Install", self.on_install_clicked)
        self.continue_button = self.add_button("_Continue")
        self.play_button = self.add_button("_Launch", self.launch_game)
        self.close_button = self.add_button("_Close", self.on_destroy)

        self.continue_handler = None

        self.get_scripts()

        self.present()

    def add_button(self, label, handler=None):
        button = Gtk.Button.new_with_mnemonic(label)
        if handler:
            button.connect("clicked", handler)
        self.action_buttons.add(button)
        return button

    def get_scripts(self):
        if system.path_exists(self.installer_file):
            # local script
            self.on_scripts_obtained(interpreter.read_script(self.installer_file))
        else:
            jobs.AsyncCall(
                interpreter.fetch_script,
                self.on_scripts_obtained,
                self.game_slug,
                self.revision,
            )

    def on_scripts_obtained(self, scripts, _error=None):
        if not scripts:
            self.destroy()
            self.run_no_installer_dialog()
            return

        if not isinstance(scripts, list):
            scripts = [scripts]
        self.scripts = scripts
        self.show_all()
        self.close_button.hide()
        self.play_button.hide()
        self.install_button.hide()
        self.source_button.hide()
        self.eject_button.hide()
        self.continue_button.hide()
        self.install_in_progress = True

        self.choose_installer()

    def run_no_installer_dialog(self):
        """Open dialog for 'no script available' situation."""
        dlg = NoInstallerDialog(self)
        if dlg.result == dlg.MANUAL_CONF:
            game_data = pga.get_game_by_field(self.game_slug, "slug")

            if game_data and "slug" in game_data:
                # Game data already exist locally.
                game = Game(game_data["id"])
            else:
                # Try to load game data from remote.
                games = api.get_api_games([self.game_slug])

                if games and len(games) >= 1:
                    remote_game = games[0]
                    game_data = {
                        "name": remote_game["name"],
                        "slug": remote_game["slug"],
                        "year": remote_game["year"],
                        "updated": remote_game["updated"],
                        "steamid": remote_game["steamid"],
                    }
                    game = Game(pga.add_game(**game_data))
                else:
                    game = None
            AddGameDialog(self.parent, game=game)
        elif dlg.result == dlg.NEW_INSTALLER:
            webbrowser.open(settings.GAME_URL % self.game_slug)

    def validate_scripts(self):
        """Auto-fixes some script aspects and checks for mandatory fields"""
        for script in self.scripts:
            for item in ["description", "notes"]:
                script[item] = script.get(item) or ""
            for item in ["name", "runner", "version"]:
                if item not in script:
                    logger.error("Invalid script: %s", script)
                    raise ScriptingError('Missing field "%s" in install script' % item)

    def choose_installer(self):
        """Stage where we choose an install script."""
        self.validate_scripts()
        base_script = self.scripts[0]
        self.title_label.set_markup("<b>Install %s</b>" % escape_gtk_label(base_script["name"]))
        installer_picker = InstallerPicker(self.scripts)
        installer_picker.connect("installer-selected", self.on_installer_selected)
        scrolledwindow = Gtk.ScrolledWindow(
            hexpand=True, vexpand=True, child=installer_picker
        )
        scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self.widget_box.pack_end(scrolledwindow, True, True, 10)
        scrolledwindow.show()

    def prepare_install(self, script_slug):
        install_script = None
        for script in self.scripts:
            if script["slug"] == script_slug:
                install_script = script
        if not install_script:
            raise ValueError("Could not find script %s" % script_slug)
        try:
            self.interpreter = interpreter.ScriptInterpreter(install_script, self)
        except MissingGameDependency as ex:
            dlg = QuestionDialog(
                {
                    "question": "This game requires %s. Do you want to install it?" % ex.slug,
                    "title": "Missing dependency",
                }
            )
            if dlg.result == Gtk.ResponseType.YES:
                InstallerWindow(
                    game_slug=ex.slug,
                    parent=self.parent,
                    application=self.application,
                )
            self.destroy()
            return

        self.title_label.set_markup(
            u"<b>Installing {}</b>".format(
                escape_gtk_label(self.interpreter.game_name)
            )
        )
        self.select_install_folder()

    def select_install_folder(self):
        """Stage where we select the install directory."""
        if self.interpreter.creates_game_folder:
            self.set_message("Select installation directory")
            default_path = self.interpreter.get_default_target()
            self.set_path_chooser(self.on_target_changed, "folder", default_path)

        else:
            self.set_message("Click install to continue")
        if self.continue_handler:
            self.continue_button.disconnect(self.continue_handler)
        self.continue_button.hide()
        self.source_button.show()
        self.install_button.grab_focus()
        self.install_button.show()

    def on_installer_selected(self, widget, installer_slug):
        self.clean_widgets()
        self.prepare_install(installer_slug)

    def on_target_changed(self, text_entry, _data):
        """Set the installation target for the game."""
        self.interpreter.target_path = os.path.expanduser(text_entry.get_text())

    def on_install_clicked(self, button):
        """Let the interpreter take charge of the next stages."""
        button.hide()
        self.source_button.hide()
        self.interpreter.check_runner_install()

    def ask_user_for_file(self, message):
        self.clean_widgets()
        self.set_message(message)
        path = self.selected_directory or os.path.expanduser("~")
        self.set_path_chooser(
            self.continue_guard,
            "file",
            default_path=path
        )

    def continue_guard(self, _, action):
        """This is weird and needs to be explained."""
        path = os.path.expanduser(self.location_entry.get_text())
        if (
                action == Gtk.FileChooserAction.OPEN and os.path.isfile(path)
        ) or (
                action == Gtk.FileChooserAction.SELECT_FOLDER and os.path.isdir(path)
        ):
            self.continue_button.set_sensitive(True)
            self.continue_button.connect("clicked", self.on_file_selected)
            self.continue_button.grab_focus()
        else:
            self.continue_button.set_sensitive(False)

    def set_path_chooser(self, callback_on_changed, action=None, default_path=None):
        """Display a file/folder chooser."""
        self.install_button.set_visible(False)
        self.continue_button.show()
        self.continue_button.set_sensitive(False)

        if action == "file":
            title = "Select file"
            action = Gtk.FileChooserAction.OPEN
            enable_warnings = False
        elif action == "folder":
            title = "Select folder"
            action = Gtk.FileChooserAction.SELECT_FOLDER
            enable_warnings = True
        else:
            raise ValueError("Invalid action %s", action)

        if self.location_entry:
            self.location_entry.destroy()
        self.location_entry = FileChooserEntry(
            title,
            action,
            path=default_path,
            warn_if_non_empty=enable_warnings,
            warn_if_ntfs=enable_warnings
        )
        self.location_entry.entry.connect("changed", callback_on_changed, action)
        self.widget_box.pack_start(self.location_entry, False, False, 0)

    def on_file_selected(self, widget):
        file_path = os.path.expanduser(self.location_entry.get_text())
        if os.path.isfile(file_path):
            self.selected_directory = os.path.dirname(file_path)
        else:
            logger.warning("%s is not a file", file_path)
            return
        self.interpreter.file_selected(file_path)

    def start_download(
        self, file_uri, dest_file, callback=None, data=None, referer=None
    ):
        self.clean_widgets()
        logger.debug("Downloading %s to %s", file_uri, dest_file)
        self.download_progress = DownloadProgressBox(
            {"url": file_uri, "dest": dest_file, "referer": referer}, cancelable=True
        )
        self.download_progress.cancel_button.hide()
        self.download_progress.connect("complete", self.on_download_complete, callback, data)
        self.widget_box.pack_start(self.download_progress, False, False, 10)
        self.download_progress.show()
        self.download_progress.start()
        self.interpreter.abort_current_task = self.download_progress.cancel

    def on_download_complete(self, _widget, _data, callback=None, callback_data=None):
        """Action called on a completed download."""
        if callback:
            try:
                callback_data = callback_data or {}
                callback(**callback_data)
            except Exception as ex:  # pylint: disable:broad-except
                raise ScriptingError(str(ex))

        self.interpreter.abort_current_task = None
        self.interpreter.iter_game_files()

    def ask_for_disc(self, message, callback, requires):
        """Ask the user to do insert a CD-ROM."""
        time.sleep(0.3)
        self.clean_widgets()
        label = Gtk.Label(label=message)
        label.set_use_markup(True)
        self.widget_box.add(label)
        label.show()

        buttons_box = Gtk.Box()
        buttons_box.show()
        buttons_box.set_margin_top(40)
        buttons_box.set_margin_bottom(40)
        self.widget_box.add(buttons_box)

        autodetect_button = Gtk.Button(label="Autodetect")
        autodetect_button.connect("clicked", callback, requires)
        autodetect_button.grab_focus()
        autodetect_button.show()
        buttons_box.pack_start(autodetect_button, True, True, 40)

        browse_button = Gtk.Button(label="Browse…")
        callback_data = {"callback": callback, "requires": requires}
        browse_button.connect("clicked", self.on_browse_clicked, callback_data)
        browse_button.show()
        buttons_box.pack_start(browse_button, True, True, 40)

    def on_browse_clicked(self, widget, callback_data):
        dialog = DirectoryDialog(
            "Select the folder where the disc is mounted", parent=self
        )
        folder = dialog.folder
        callback = callback_data["callback"]
        requires = callback_data["requires"]
        callback(widget, requires, folder)

    def on_eject_clicked(self, widget, data=None):
        self.interpreter.eject_wine_disc()

    def input_menu(self, alias, options, preselect, has_entry, callback):
        """Display an input request as a dropdown menu with options."""
        time.sleep(0.3)
        self.clean_widgets()

        model = Gtk.ListStore(str, str)
        for option in options:
            key, label = option.popitem()
            model.append([key, label])
        combobox = Gtk.ComboBox.new_with_model(model)
        renderer_text = Gtk.CellRendererText()
        combobox.pack_start(renderer_text, True)
        combobox.add_attribute(renderer_text, "text", 1)
        combobox.set_id_column(0)
        combobox.set_active_id(preselect)
        combobox.set_halign(Gtk.Align.CENTER)
        self.widget_box.pack_start(combobox, True, False, 100)

        combobox.connect("changed", self.on_input_menu_changed)
        combobox.show()
        self.continue_handler = self.continue_button.connect(
            "clicked", callback, alias, combobox
        )
        self.continue_button.grab_focus()
        self.continue_button.show()

        self.on_input_menu_changed(combobox)

    def on_input_menu_changed(self, widget):
        """Enable continue button if a non-empty choice is selected"""
        self.continue_button.set_sensitive(bool(widget.get_active_id()))

    def on_install_finished(self):
        self.clean_widgets()
        self.install_in_progress = False

        self.desktop_shortcut_box = Gtk.CheckButton("Create desktop shortcut")
        self.menu_shortcut_box = Gtk.CheckButton("Create application menu " "shortcut")
        self.widget_box.pack_start(self.desktop_shortcut_box, False, False, 5)
        self.widget_box.pack_start(self.menu_shortcut_box, False, False, 5)
        self.widget_box.show_all()

        if settings.read_setting("create_desktop_shortcut") == "True":
            self.desktop_shortcut_box.set_active(True)
        if settings.read_setting("create_menu_shortcut") == "True":
            self.menu_shortcut_box.set_active(True)

        self.connect("delete-event", self.create_shortcuts)

        self.eject_button.hide()
        self.cancel_button.hide()
        self.continue_button.hide()
        self.install_button.hide()
        self.play_button.show()
        self.close_button.grab_focus()
        self.close_button.show()

        if not self.is_active():
            self.set_urgency_hint(True)  # Blink in taskbar
            self.connect("focus-in-event", self.on_window_focus)

    def on_window_focus(self, widget, *args):
        self.set_urgency_hint(False)

    def on_install_error(self, message):
        self.set_status(message)
        self.clean_widgets()
        self.cancel_button.grab_focus()

    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.game_id))

    def on_destroy(self, _widget, _data=None):
        """destroy event handler"""
        if self.install_in_progress:
            abort_close = self.cancel_installation()
            if abort_close:
                return True
        else:
            if self.interpreter:
                self.interpreter.cleanup()
            self.destroy()

    def create_shortcuts(self, *args):
        """Create desktop and global menu shortcuts."""
        game_slug = self.interpreter.game_slug
        game_id = self.interpreter.game_id
        game_name = self.interpreter.game_name
        create_desktop_shortcut = self.desktop_shortcut_box.get_active()
        create_menu_shortcut = self.menu_shortcut_box.get_active()

        if create_desktop_shortcut:
            xdgshortcuts.create_launcher(game_slug, game_id, game_name, desktop=True)
        if create_menu_shortcut:
            xdgshortcuts.create_launcher(game_slug, game_id, game_name, menu=True)

        settings.write_setting("create_desktop_shortcut", create_desktop_shortcut)
        settings.write_setting("create_menu_shortcut", create_menu_shortcut)

    def cancel_installation(self, widget=None):
        """Ask a confirmation before cancelling the install"""
        confirm_cancel_dialog = QuestionDialog(
            {
                "question": "Are you sure you want to cancel the installation?",
                "title": "Cancel installation?",
            }
        )
        if confirm_cancel_dialog.result != Gtk.ResponseType.YES:
            logger.warning("Attempting to terminate with the system wineserver. "
                           "This is most likely to fail or to have no effect.")
            system.execute([system.find_executable("wineserver"), "-k9"])
            return True
        if self.interpreter:
            self.interpreter.revert()
            self.interpreter.cleanup()
        self.destroy()

    def on_source_clicked(self, _button):
        InstallerSourceDialog(
            self.interpreter.script_pretty, self.interpreter.game_name, self
        )

    def clean_widgets(self):
        """Cleanup before displaying the next stage."""
        for child_widget in self.widget_box.get_children():
            child_widget.destroy()

    def set_status(self, text):
        """Display a short status text."""
        self.status_label.set_text(text)

    def set_message(self, message):
        """Display a message."""
        label = Gtk.Label()
        label.set_markup("<b>%s</b>" % add_url_tags(message))
        label.set_max_width_chars(80)
        label.set_property("wrap", True)
        label.set_alignment(0, 0)
        label.show()
        self.widget_box.pack_start(label, False, False, 18)

    def add_spinner(self):
        """Display a wait icon."""
        self.clean_widgets()
        spinner = Gtk.Spinner()
        self.widget_box.pack_start(spinner, False, False, 18)
        spinner.show()
        spinner.start()

    def attach_logger(self, command):
        """Creates a TextBuffer and attach it to a command"""
        self.log_buffer = Gtk.TextBuffer()
        command.set_log_buffer(self.log_buffer)
        self.log_textview = LogTextView(self.log_buffer)
        scrolledwindow = Gtk.ScrolledWindow(
            hexpand=True, vexpand=True, child=self.log_textview
        )
        scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self.widget_box.pack_end(scrolledwindow, True, True, 10)
        scrolledwindow.show()
        self.log_textview.show()
コード例 #17
0
class InstallerWindow(Gtk.ApplicationWindow):
    """GUI for the install process."""

    game_dir = None
    download_progress = None

    def __init__(
        self,
        game_slug=None,
        installer_file=None,
        revision=None,
        parent=None,
        application=None,
    ):
        Gtk.ApplicationWindow.__init__(self,
                                       icon_name="lutris",
                                       application=application)
        self.application = application
        self.set_show_menubar(False)
        self.interpreter = None
        self.selected_directory = None  # Latest directory chosen by user
        self.parent = parent
        self.game_slug = game_slug
        self.installer_file = installer_file
        self.revision = revision

        self.log_buffer = None
        self.log_textview = None

        # Dialog properties
        self.set_size_request(420, 420)
        self.set_default_size(600, 480)
        self.set_position(Gtk.WindowPosition.CENTER)

        self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
        self.vbox.set_margin_top(18)
        self.vbox.set_margin_bottom(18)
        self.vbox.set_margin_right(18)
        self.vbox.set_margin_left(18)
        self.add(self.vbox)

        # Default signals
        self.connect("destroy", self.on_destroy)

        # GUI Setup

        # Title label
        self.title_label = Gtk.Label()
        self.vbox.add(self.title_label)

        self.status_label = Gtk.Label()
        self.status_label.set_max_width_chars(80)
        self.status_label.set_property("wrap", True)
        self.status_label.set_selectable(True)
        self.vbox.add(self.status_label)

        # Main widget box
        self.widget_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.vbox.pack_start(self.widget_box, True, True, 0)

        self.location_entry = None

        # Separator
        self.vbox.add(Gtk.HSeparator())

        # Buttons

        self.action_buttons = Gtk.Box(spacing=6)
        # self.action_buttons.set_margin_top(18)
        action_buttons_alignment = Gtk.Alignment.new(1, 0, 0, 0)
        action_buttons_alignment.add(self.action_buttons)
        self.vbox.pack_start(action_buttons_alignment, False, True, 0)

        self.cancel_button = Gtk.Button.new_with_mnemonic("C_ancel")
        self.cancel_button.set_tooltip_text("Abort and revert the "
                                            "installation")
        self.cancel_button.connect("clicked", self.on_cancel_clicked)
        self.action_buttons.add(self.cancel_button)

        self.eject_button = self.add_button("_Eject", self.on_eject_clicked)
        self.source_button = self.add_button("_View source",
                                             self.on_source_clicked)
        self.install_button = self.add_button("_Install",
                                              self.on_install_clicked)
        self.continue_button = self.add_button("_Continue")
        self.play_button = self.add_button("_Launch game", self.launch_game)
        self.close_button = self.add_button("_Close", self.close)

        self.continue_handler = None

        self.get_scripts()

        # l33t haxx to make Window.present() actually work.
        self.set_keep_above(True)
        self.present()
        self.set_keep_above(False)
        self.present()

    def add_button(self, label, handler=None):
        button = Gtk.Button.new_with_mnemonic(label)
        if handler:
            button.connect("clicked", handler)
        self.action_buttons.add(button)
        return button

    # ---------------------------
    # "Get installer" stage
    # ---------------------------

    def get_scripts(self):
        if system.path_exists(self.installer_file):
            # local script
            self.on_scripts_obtained(
                interpreter.read_script(self.installer_file))
        else:
            jobs.AsyncCall(
                interpreter.fetch_script,
                self.on_scripts_obtained,
                self.game_slug,
                self.revision,
            )

    def on_scripts_obtained(self, scripts, _error=None):
        if not scripts:
            self.destroy()
            self.run_no_installer_dialog()
            return

        if not isinstance(scripts, list):
            scripts = [scripts]
        self.scripts = scripts
        self.show_all()
        self.close_button.hide()
        self.play_button.hide()
        self.install_button.hide()
        self.source_button.hide()
        self.eject_button.hide()
        self.continue_button.hide()

        self.choose_installer()

    def run_no_installer_dialog(self):
        """Open dialog for 'no script available' situation."""
        dlg = NoInstallerDialog(self)
        if dlg.result == dlg.MANUAL_CONF:
            game_data = pga.get_game_by_field(self.game_slug, "slug")

            if game_data and "slug" in game_data:
                # Game data already exist locally.
                game = Game(game_data["id"])
            else:
                # Try to load game data from remote.
                games = api.get_api_games([self.game_slug])

                if games and len(games) >= 1:
                    remote_game = games[0]
                    game_data = {
                        "name": remote_game["name"],
                        "slug": remote_game["slug"],
                        "year": remote_game["year"],
                        "updated": remote_game["updated"],
                        "steamid": remote_game["steamid"],
                    }
                    game = Game(pga.add_game(**game_data))
                else:
                    game = None
            AddGameDialog(self.parent, game=game)
        elif dlg.result == dlg.NEW_INSTALLER:
            webbrowser.open(settings.GAME_URL % self.game_slug)

    # ---------------------------
    # "Choose installer" stage
    # ---------------------------
    def validate_scripts(self):
        """Auto-fixes some script aspects and checks for mandatory fields"""
        for script in self.scripts:
            for item in ["description", "notes"]:
                script[item] = script.get(item) or ""
            for item in ["name", "runner", "version"]:
                if item not in script:
                    logger.error("Invalid script: %s", script)
                    raise ScriptingError(
                        'Missing field "%s" in install script' % item)

    def choose_installer(self):
        """Stage where we choose an install script."""
        self.validate_scripts()
        base_script = self.scripts[0]
        self.title_label.set_markup("<b>Install %s</b>" % base_script["name"])
        installer_picker = InstallerPicker(self.scripts)
        installer_picker.connect("installer-selected",
                                 self.on_installer_selected)
        self.widget_box.pack_start(installer_picker, False, False, 0)

    def on_installer_selected(self, widget, installer_slug):
        self.clean_widgets()
        self.prepare_install(installer_slug)
        self.show_non_empty_warning()

    def prepare_install(self, script_slug):
        install_script = None
        for script in self.scripts:
            if script["slug"] == script_slug:
                install_script = script
        if not install_script:
            raise ValueError("Could not find script %s" % script_slug)
        self.interpreter = interpreter.ScriptInterpreter(install_script, self)
        self.title_label.set_markup(u"<b>Installing {}</b>".format(
            escape_gtk_label(self.interpreter.game_name)))
        self.select_install_folder()

    # --------------------------
    # "Select install dir" stage
    # --------------------------

    def select_install_folder(self):
        """Stage where we select the install directory."""
        if self.interpreter.creates_game_folder:
            self.set_message("Select installation directory")
            default_path = self.interpreter.get_default_target()
            self.set_path_chooser(self.on_target_changed, "folder",
                                  default_path)
            self.non_empty_label = Gtk.Label()
            self.non_empty_label.set_markup(
                "<b>Warning!</b> The selected path "
                "contains files, installation might not work properly.")
            self.widget_box.pack_start(self.non_empty_label, False, False, 10)
        else:
            self.set_message("Click install to continue")
        if self.continue_handler:
            self.continue_button.disconnect(self.continue_handler)
        self.continue_button.hide()
        self.source_button.show()
        self.install_button.grab_focus()
        self.install_button.show()

    def on_target_changed(self, text_entry, _):
        """Set the installation target for the game."""
        path = text_entry.get_text()
        self.interpreter.target_path = os.path.expanduser(path)
        self.show_non_empty_warning()

    def show_non_empty_warning(self):
        """Display a warning if destination folder is not empty."""
        if not self.location_entry:
            return
        path = self.location_entry.get_text()

        # replace ~ with full path so os.path.exists and os.listdir work correctly
        path = os.path.expanduser(path)

        if os.path.exists(path) and os.listdir(path):
            self.non_empty_label.show()
        else:
            self.non_empty_label.hide()

    # ---------------------
    # "Get the files" stage
    # ---------------------

    def on_install_clicked(self, button):
        """Let the interpreter take charge of the next stages."""
        button.hide()
        self.source_button.hide()
        self.interpreter.check_runner_install()

    def ask_user_for_file(self, message):
        self.clean_widgets()
        self.set_message(message)
        if self.selected_directory:
            path = self.selected_directory
        else:
            path = os.path.expanduser("~")
        self.set_path_chooser(self.continue_guard, "file", default_path=path)

    def continue_guard(self, _, action):

        loc = self.location_entry.get_text()
        loc = os.path.expanduser(loc)
        if (action == Gtk.FileChooserAction.OPEN and os.path.isfile(loc)) or (
                action == Gtk.FileChooserAction.SELECT_FOLDER
                and os.path.isdir(loc)):

            self.continue_button.set_sensitive(True)
            self.continue_button.connect("clicked", self.on_file_selected)
            self.continue_button.grab_focus()

        else:
            self.continue_button.set_sensitive(False)

    def set_path_chooser(self,
                         callback_on_changed,
                         action=None,
                         default_path=None):
        """Display a file/folder chooser."""

        self.install_button.set_visible(False)
        self.continue_button.show()
        self.continue_button.set_sensitive(False)

        if action == "file":
            title = "Select file"
            action = Gtk.FileChooserAction.OPEN
        elif action == "folder":
            title = "Select folder"
            action = Gtk.FileChooserAction.SELECT_FOLDER

        if self.location_entry:
            self.location_entry.destroy()
        self.location_entry = FileChooserEntry(title, action, default_path)
        self.location_entry.show_all()
        if callback_on_changed:
            self.location_entry.entry.connect("changed", callback_on_changed,
                                              action)

        self.widget_box.pack_start(self.location_entry, False, False, 0)

    def on_file_selected(self, widget):
        file_path = os.path.expanduser(self.location_entry.get_text())
        if os.path.isfile(file_path):
            self.selected_directory = os.path.dirname(file_path)
        else:
            logger.warning("%s is not a file", file_path)
            return
        self.interpreter.file_selected(file_path)

    def start_download(self,
                       file_uri,
                       dest_file,
                       callback=None,
                       data=None,
                       referer=None):
        self.clean_widgets()
        logger.debug("Downloading %s to %s", file_uri, dest_file)
        self.download_progress = DownloadProgressBox(
            {
                "url": file_uri,
                "dest": dest_file,
                "referer": referer
            },
            cancelable=True)
        self.download_progress.cancel_button.hide()
        self.download_progress.connect("complete", self.on_download_complete,
                                       callback, data)
        self.widget_box.pack_start(self.download_progress, False, False, 10)
        self.download_progress.show()
        self.download_progress.start()
        self.interpreter.abort_current_task = self.download_progress.cancel

    def on_download_complete(self,
                             _widget,
                             _data,
                             callback=None,
                             callback_data=None):
        """Action called on a completed download."""
        if callback:
            try:
                callback_data = callback_data or {}
                callback(**callback_data)
            except Exception as ex:  # pylint: disable:broad-except
                raise ScriptingError(str(ex))

        self.interpreter.abort_current_task = None
        self.interpreter.iter_game_files()

    # ----------------
    # "Commands" stage
    # ----------------

    def ask_for_disc(self, message, callback, requires):
        """Ask the user to do insert a CD-ROM."""
        time.sleep(0.3)
        self.clean_widgets()
        label = Gtk.Label(label=message)
        label.set_use_markup(True)
        self.widget_box.add(label)
        label.show()

        buttons_box = Gtk.Box()
        buttons_box.show()
        buttons_box.set_margin_top(40)
        buttons_box.set_margin_bottom(40)
        self.widget_box.add(buttons_box)

        autodetect_button = Gtk.Button(label="Autodetect")
        autodetect_button.connect("clicked", callback, requires)
        autodetect_button.grab_focus()
        autodetect_button.show()
        buttons_box.pack_start(autodetect_button, True, True, 40)

        browse_button = Gtk.Button(label="Browse…")
        callback_data = {"callback": callback, "requires": requires}
        browse_button.connect("clicked", self.on_browse_clicked, callback_data)
        browse_button.show()
        buttons_box.pack_start(browse_button, True, True, 40)

    def on_browse_clicked(self, widget, callback_data):
        dialog = DirectoryDialog("Select the folder where the disc is mounted",
                                 parent=self)
        folder = dialog.folder
        callback = callback_data["callback"]
        requires = callback_data["requires"]
        callback(widget, requires, folder)

    def on_eject_clicked(self, widget, data=None):
        self.interpreter.eject_wine_disc()

    def input_menu(self, alias, options, preselect, has_entry, callback):
        """Display an input request as a dropdown menu with options."""
        time.sleep(0.3)
        self.clean_widgets()

        model = Gtk.ListStore(str, str)
        for option in options:
            key, label = option.popitem()
            model.append([key, label])
        combobox = Gtk.ComboBox.new_with_model(model)
        renderer_text = Gtk.CellRendererText()
        combobox.pack_start(renderer_text, True)
        combobox.add_attribute(renderer_text, "text", 1)
        combobox.set_id_column(0)
        combobox.set_active_id(preselect)
        combobox.set_halign(Gtk.Align.CENTER)
        self.widget_box.pack_start(combobox, True, False, 100)

        combobox.connect("changed", self.on_input_menu_changed)
        combobox.show()
        self.continue_handler = self.continue_button.connect(
            "clicked", callback, alias, combobox)
        self.continue_button.grab_focus()
        self.continue_button.show()

        self.on_input_menu_changed(combobox)

    def on_input_menu_changed(self, widget):
        # Enable continue button if a non-empty choice is selected
        self.continue_button.set_sensitive(bool(widget.get_active_id()))

    # ----------------
    # "Finalize" stage
    # ----------------

    def on_install_finished(self):
        """Actual game installation."""
        self.notify_install_success()
        self.clean_widgets()

        # Shortcut checkboxes
        self.desktop_shortcut_box = Gtk.CheckButton("Create desktop shortcut")
        self.menu_shortcut_box = Gtk.CheckButton("Create application menu "
                                                 "shortcut")
        self.widget_box.pack_start(self.desktop_shortcut_box, False, False, 5)
        self.widget_box.pack_start(self.menu_shortcut_box, False, False, 5)
        self.widget_box.show_all()

        if settings.read_setting("create_desktop_shortcut") == "True":
            self.desktop_shortcut_box.set_active(True)
        if settings.read_setting("create_menu_shortcut") == "True":
            self.menu_shortcut_box.set_active(True)

        self.connect("destroy", self.create_shortcuts)

        # Buttons
        self.eject_button.hide()
        self.cancel_button.hide()
        self.continue_button.hide()
        self.install_button.hide()
        self.play_button.show()
        self.close_button.grab_focus()
        self.close_button.show()

        if not self.is_active():
            self.set_urgency_hint(True)  # Blink in taskbar
            self.connect("focus-in-event", self.on_window_focus)

    def notify_install_success(self, game_id=None):

        # Nothing to notify in case of extends scripts
        if self.interpreter.extends:
            return

        game_id = game_id or self.interpreter.game_id
        if self.parent:
            self.parent.view.emit("game-installed", game_id)

    def on_window_focus(self, widget, *args):
        self.set_urgency_hint(False)

    def on_install_error(self, message):
        self.set_status(message)
        self.clean_widgets()
        self.cancel_button.grab_focus()

    # --------------------
    # "Afer the end" stage
    # --------------------

    def launch_game(self, widget, _data=None):
        """Launch a game after it's been installed."""
        widget.set_sensitive(False)
        self.close(widget)
        self.application.launch(Game(self.interpreter.game_id))

    def close(self, _widget):
        self.destroy()

    def on_destroy(self, widget):
        """destroy event handler"""
        if self.interpreter:
            self.interpreter.cleanup()
        self.destroy()

    def create_shortcuts(self, *args):
        """Create desktop and global menu shortcuts."""
        game_slug = self.interpreter.game_slug
        game_id = self.interpreter.game_id
        game_name = self.interpreter.game_name
        create_desktop_shortcut = self.desktop_shortcut_box.get_active()
        create_menu_shortcut = self.menu_shortcut_box.get_active()

        if create_desktop_shortcut:
            xdgshortcuts.create_launcher(game_slug,
                                         game_id,
                                         game_name,
                                         desktop=True)
        if create_menu_shortcut:
            xdgshortcuts.create_launcher(game_slug,
                                         game_id,
                                         game_name,
                                         menu=True)

        settings.write_setting("create_desktop_shortcut",
                               create_desktop_shortcut)
        settings.write_setting("create_menu_shortcut", create_menu_shortcut)

    # --------------
    # Cancel install
    # --------------

    def on_cancel_clicked(self, _button):
        if self.interpreter:
            self.interpreter.revert()
        self.destroy()

    # -------------
    # View Source
    # -------------

    def on_source_clicked(self, _button):
        InstallerSourceDialog(self.interpreter.script_pretty,
                              self.interpreter.game_name, self)

    # -------------
    # Utility stuff
    # -------------

    def clean_widgets(self):
        """Cleanup before displaying the next stage."""
        for child_widget in self.widget_box.get_children():
            child_widget.destroy()

    def set_status(self, text):
        """Display a short status text."""
        self.status_label.set_text(text)

    def set_message(self, message):
        """Display a message."""
        label = Gtk.Label()
        label.set_markup("<b>%s</b>" % add_url_tags(message))
        label.set_max_width_chars(80)
        label.set_property("wrap", True)
        label.set_alignment(0, 0)
        label.show()
        self.widget_box.pack_start(label, False, False, 18)

    def add_spinner(self):
        """Display a wait icon."""
        self.clean_widgets()
        spinner = Gtk.Spinner()
        self.widget_box.pack_start(spinner, False, False, 18)
        spinner.show()
        spinner.start()

    def attach_logger(self, command):
        """Creates a TextBuffer and attach it to a command"""
        self.log_buffer = Gtk.TextBuffer()
        command.set_log_buffer(self.log_buffer)
        self.log_textview = LogTextView(self.log_buffer)
        scrolledwindow = Gtk.ScrolledWindow(hexpand=True,
                                            vexpand=True,
                                            child=self.log_textview)
        scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self.widget_box.pack_end(scrolledwindow, True, True, 10)
        scrolledwindow.show()
        self.log_textview.show()