Repository: lutris/lutris

Issue:
How do I create a win32 prefix for my game?
I'm trying to install a game using "Install a Windows game from media". After I press "Install" button in the dialog, I see this in the console:

```
win64 Prefix created in /path/to/a/prefix
```

But I want my game to be installed in a win32 prefix. I was never given an option to select this for my game. I don't see such an option in the Wine runner options too. So, how do I do this?


Discussion:

Resolving patch:
diff --git a/lutris/gui/addgameswindow.py b/lutris/gui/addgameswindow.py
--- a/lutris/gui/addgameswindow.py
+++ b/lutris/gui/addgameswindow.py
@@ -1,3 +1,4 @@
+import os
 from gettext import gettext as _
 
 from gi.repository import Gio, GLib, Gtk
@@ -5,11 +6,15 @@
 from lutris import api
 from lutris.exceptions import watch_errors
 from lutris.gui.config.add_game import AddGameDialog
-from lutris.gui.dialogs import DirectoryDialog, ErrorDialog, FileDialog
+from lutris.gui.dialogs import ErrorDialog
+from lutris.gui.dialogs.game_import import ImportGameDialog
+from lutris.gui.widgets.common import FileChooserEntry
+from lutris.gui.widgets.navigation_stack import NavigationStack
 from lutris.gui.widgets.window import BaseApplicationWindow
 from lutris.installer import AUTO_WIN32_EXE, get_installers
 from lutris.scanners.lutris import scan_directory
 from lutris.util.jobs import AsyncCall
+from lutris.util.linux import LINUX_SYSTEM
 from lutris.util.strings import gtk_safe, slugify
 
 
@@ -19,30 +24,42 @@ class AddGamesWindow(BaseApplicationWindow):  # pylint: disable=too-many-public-
     sections = [
         (
             "system-search-symbolic",
+            "go-next-symbolic",
             _("Search the Lutris website for installers"),
             _("Query our website for community installers"),
-            "search_installers"
+            "search_installers",
         ),
         (
             "folder-new-symbolic",
+            "go-next-symbolic",
             _("Scan a folder for games"),
             _("Mass-import a folder of games"),
             "scan_folder"
         ),
         (
             "media-optical-dvd-symbolic",
+            "go-next-symbolic",
             _("Install a Windows game from media"),
             _("Launch a setup file from an optical drive or download"),
             "install_from_setup"
         ),
         (
             "x-office-document-symbolic",
+            "go-next-symbolic",
             _("Install from a local install script"),
             _("Run a YAML install script"),
             "install_from_script"
         ),
+        (
+            "application-x-firmware-symbolic",
+            "go-next-symbolic",
+            _("Import a ROM"),
+            _("Import a ROM that is known to Lutris"),
+            "import_rom"
+        ),
         (
             "list-add-symbolic",
+            "view-more-horizontal-symbolic",
             _("Add locally installed game"),
             _("Manually configure a game available locally"),
             "add_local_game"
@@ -54,140 +71,167 @@ class AddGamesWindow(BaseApplicationWindow):  # pylint: disable=too-many-public-
     def __init__(self, application=None):
         super().__init__(application=application)
         self.set_default_size(640, 450)
+        self.search_entry = None
+        self.search_frame = None
+        self.search_explanation_label = None
+        self.search_listbox = None
         self.search_timer_id = None
         self.search_spinner = None
         self.text_query = None
-        self.result_label = None
+        self.search_result_label = None
         self.title_label = Gtk.Label(visible=True)
-        self.title_label.set_markup(f"<b>{self.title_text}</b>")
-        self.vbox.pack_start(self.title_label, False, False, 12)
+        self.vbox.pack_start(self.title_label, False, False, 0)
 
-        self.listbox = Gtk.ListBox(visible=True)
-        self.listbox.set_activate_on_single_click(True)
-        self.vbox.pack_start(self.listbox, False, False, 12)
-        for icon, text, subtext, callback_name in self.sections:
-            row = self.build_row(icon, text, subtext)
-            row.callback_name = callback_name
+        self.accelerators = Gtk.AccelGroup()
+        self.add_accel_group(self.accelerators)
+
+        self.back_button = Gtk.Button(_("Back"), sensitive=False)
+        self.back_button.connect("clicked", self.on_back_clicked)
+        key, mod = Gtk.accelerator_parse("<Alt>Left")
+        self.back_button.add_accelerator("clicked", self.accelerators, key, mod, Gtk.AccelFlags.VISIBLE)
+        key, mod = Gtk.accelerator_parse("<Alt>Home")
+        self.accelerators.connect(key, mod, Gtk.AccelFlags.VISIBLE, self.on_navigate_home)
+        self.action_buttons.pack_start(self.back_button, False, False, 0)
+
+        self.continue_button = Gtk.Button(_("_Continue"), no_show_all=True, use_underline=True)
+        self.action_buttons.pack_end(self.continue_button, False, False, 0)
+        self.continue_handler = None
+
+        self.cancel_button = Gtk.Button(_("Cancel"), use_underline=True)
+        self.cancel_button.connect("clicked", self.on_cancel_clicked)
+        key, mod = Gtk.accelerator_parse("Escape")
+        self.cancel_button.add_accelerator("clicked", self.accelerators, key, mod, Gtk.AccelFlags.VISIBLE)
+        self.action_buttons.pack_end(self.cancel_button, False, False, 0)
 
-            self.listbox.add(row)
-        self.listbox.connect("row-activated", self.on_row_activated)
+        self.stack = NavigationStack(self.back_button)
+        self.vbox.pack_start(self.stack, True, True, 0)
+
+        self.vbox.pack_start(Gtk.HSeparator(), False, False, 0)
+
+        # Pre-create some controls so they can be used in signal handlers
+
+        self.scan_directory_chooser = FileChooserEntry(
+            title=_("Select folder"), action=Gtk.FileChooserAction.SELECT_FOLDER
+        )
+
+        self.install_from_setup_game_name_entry = Gtk.Entry()
+        self.install_from_setup_game_slug_checkbox = Gtk.CheckButton(label="Identifier")
+        self.install_from_setup_game_slug_entry = Gtk.Entry(sensitive=False)
+        self.install_from_setup_32bit_prefix_checkbox = None
+
+        self.install_script_file_chooser = FileChooserEntry(
+            title=_("Select script"), action=Gtk.FileChooserAction.OPEN
+        )
+
+        self.import_rom_file_chooser = FileChooserEntry(
+            title=_("Select ROM file"), action=Gtk.FileChooserAction.OPEN
+        )
+
+        self.stack.add_named_factory("initial", self.create_initial_page)
+        self.stack.add_named_factory("search_installers", self.create_search_installers_page)
+        self.stack.add_named_factory("scan_folder", self.create_scan_folder_page)
+        self.stack.add_named_factory("scanning_folder", self.create_scanning_folder_page)
+        self.stack.add_named_factory("installed_games", self.create_installed_games_page)
+        self.stack.add_named_factory("install_from_setup", self.create_install_from_setup_page)
+        self.stack.add_named_factory("install_from_script", self.create_install_from_script_page)
+        self.stack.add_named_factory("import_rom", self.create_import_rom_page)
+
+        self.show_all()
+
+        self.load_initial_page()
+
+    def on_back_clicked(self, _widget):
+        self.stack.navigate_back()
+
+    def on_navigate_home(self, _accel_group, _window, _keyval, _modifier):
+        self.stack.navigate_home()
+
+    def on_cancel_clicked(self, _widget):
+        self.destroy()
 
     def on_watched_error(self, error):
         ErrorDialog(str(error), parent=self)
 
+    # Initial Page
+
+    def load_initial_page(self):
+        self.stack.navigate_to_page(self.present_inital_page)
+
+    def create_initial_page(self):
+        frame = Gtk.Frame(shadow_type=Gtk.ShadowType.ETCHED_IN)
+        listbox = Gtk.ListBox()
+        listbox.set_activate_on_single_click(True)
+        for icon, next_icon, text, subtext, callback_name in self.sections:
+            row = self._get_listbox_row(icon, text, subtext, next_icon)
+            row.callback_name = callback_name
+
+            listbox.add(row)
+        listbox.connect("row-activated", self.on_row_activated)
+        frame.add(listbox)
+        return frame
+
+    def present_inital_page(self):
+        self.title_label.set_markup(f"<b>{self.title_text}</b>")
+        self.stack.present_page("initial")
+        self.display_cancel_button()
+
     @watch_errors()
     def on_row_activated(self, listbox, row):
         if row.callback_name:
             callback = getattr(self, row.callback_name)
             callback()
 
-    def _get_row(self):
-        row = Gtk.ListBoxRow(visible=True)
-        row.set_selectable(False)
-        row.set_activatable(True)
-        return row
-
-    def _get_box(self):
-        return Gtk.Box(
-            spacing=12,
-            margin_right=12,
-            margin_left=12,
-            margin_top=12,
-            margin_bottom=12,
-            visible=True,
-        )
-
-    def _get_icon(self, name, small=False):
-        if small:
-            size = Gtk.IconSize.MENU
-        else:
-            size = Gtk.IconSize.DND
-        icon = Gtk.Image.new_from_icon_name(name, size)
-        icon.show()
-        return icon
-
-    def _get_label(self, text):
-        label = Gtk.Label(visible=True)
-        label.set_markup(text)
-        label.set_alignment(0, 0.5)
-        return label
-
-    def build_row(self, icon_name, text, subtext):
-        row = self._get_row()
-        box = self._get_box()
-        if icon_name:
-            icon = self._get_icon(icon_name)
-            box.pack_start(icon, False, False, 0)
-        label = self._get_label(f"<b>{text}</b>\n{subtext}")
-        box.pack_start(label, True, True, 0)
-        if icon_name:
-            next_icon = self._get_icon("go-next-symbolic", small=True)
-            box.pack_start(next_icon, False, False, 0)
-        row.add(box)
-        return row
+    # Search Installers Page
 
     def search_installers(self):
         """Search installers with the Lutris API"""
-        self.title_label.set_markup(_("<b>Search Lutris.net</b>"))
-        self.listbox.destroy()
-        hbox = Gtk.Box(Gtk.Orientation.HORIZONTAL, visible=True)
-        entry = Gtk.SearchEntry(visible=True)
-        hbox.pack_start(entry, True, True, 0)
+        if self.search_entry:
+            self.search_entry.set_text("")
+            self.search_result_label.set_text("")
+            self.search_result_label.hide()
+            self.search_frame.hide()
+            self.search_explanation_label.show()
+        self.stack.navigate_to_page(self.present_search_installers_page)
+
+    def create_search_installers_page(self):
+        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, no_show_all=True, spacing=6, visible=True)
+        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, visible=True)
+        self.search_entry = Gtk.SearchEntry(visible=True)
+        hbox.pack_start(self.search_entry, True, True, 0)
         self.search_spinner = Gtk.Spinner(visible=False)
         hbox.pack_end(self.search_spinner, False, False, 6)
-        self.vbox.add(hbox)
-        self.result_label = self._get_label("")
-        self.vbox.add(self.result_label)
-        entry.connect("changed", self._on_search_updated)
-        self.listbox = Gtk.ListBox()
-        self.listbox.connect("row-activated", self._on_game_selected)
+        vbox.pack_start(hbox, False, False, 0)
+        self.search_result_label = self._get_label("")
+        self.search_result_label.hide()
+        vbox.pack_start(self.search_result_label, False, False, 0)
+        self.search_entry.connect("changed", self._on_search_updated)
+
+        explanation = _(
+            "Lutris will search Lutris.net for games matching the terms you enter, and any "
+            "that it finds will appear here.\n\n"
+            "When you click on a game that it found, the installer window will appear to "
+            "perform the installation."
+        )
+
+        self.search_explanation_label = self._get_explanation_label(explanation)
+        vbox.add(self.search_explanation_label)
+
+        self.search_frame = Gtk.Frame(shadow_type=Gtk.ShadowType.ETCHED_IN)
+        self.search_listbox = Gtk.ListBox(visible=True)
+        self.search_listbox.connect("row-activated", self._on_game_selected)
         scroll = Gtk.ScrolledWindow(visible=True)
         scroll.set_vexpand(True)
-        scroll.add(self.listbox)
-        self.vbox.add(scroll)
-        entry.grab_focus()
+        scroll.add(self.search_listbox)
+        self.search_frame.add(scroll)
 
-    def scan_folder(self):
-        """Scan a folder of already installed games"""
-        self.title_label.set_markup("<b>Import games from a folder</b>")
-        self.listbox.destroy()
-        script_dlg = DirectoryDialog(_("Select folder to scan"))
-        if not script_dlg.folder:
-            self.destroy()
-            return
-        spinner = Gtk.Spinner(visible=True)
-        spinner.start()
-        self.vbox.pack_start(spinner, False, False, 18)
-        AsyncCall(scan_directory, self._on_folder_scanned, script_dlg.folder)
+        vbox.pack_start(self.search_frame, True, True, 0)
+        return vbox
 
-    @watch_errors()
-    def _on_folder_scanned(self, result, error):
-        if error:
-            ErrorDialog(str(error), parent=self)
-            self.destroy()
-            return
-        for child in self.vbox.get_children():
-            child.destroy()
-        installed, missing = result
-        installed_label = self._get_label("Installed games")
-        self.vbox.add(installed_label)
-        installed_listbox = Gtk.ListBox(visible=True)
-        installed_scroll = Gtk.ScrolledWindow(visible=True)
-        installed_scroll.set_vexpand(True)
-        installed_scroll.add(installed_listbox)
-        self.vbox.add(installed_scroll)
-        for folder in installed:
-            installed_listbox.add(self.build_row("", gtk_safe(folder), ""))
-
-        missing_label = self._get_label("No match found")
-        self.vbox.add(missing_label)
-        missing_listbox = Gtk.ListBox(visible=True)
-        missing_scroll = Gtk.ScrolledWindow(visible=True)
-        missing_scroll.set_vexpand(True)
-        missing_scroll.add(missing_listbox)
-        self.vbox.add(missing_scroll)
-        for folder in missing:
-            missing_listbox.add(self.build_row("", gtk_safe(folder), ""))
+    def present_search_installers_page(self):
+        self.title_label.set_markup(_("<b>Search Lutris.net</b>"))
+        self.stack.present_page("search_installers")
+        self.search_entry.grab_focus()
+        self.display_cancel_button()
 
     @watch_errors()
     def _on_search_updated(self, entry):
@@ -196,14 +240,6 @@ def _on_search_updated(self, entry):
         self.text_query = entry.get_text().strip()
         self.search_timer_id = GLib.timeout_add(750, self.update_search_results)
 
-    @watch_errors()
-    def _on_game_selected(self, listbox, row):
-        game_slug = row.api_info["slug"]
-        installers = get_installers(game_slug=game_slug)
-        application = Gio.Application.get_default()
-        application.show_installer_window(installers)
-        self.destroy()
-
     @watch_errors()
     def update_search_results(self):
         # Don't start a search while another is going; defer it instead.
@@ -229,12 +265,12 @@ def update_search_results_cb(self, api_games, error):
         count = len(api_games.get('results', []))
 
         if not count:
-            self.result_label.set_markup(_("No results"))
+            self.search_result_label.set_markup(_("No results"))
         elif count == total_count:
-            self.result_label.set_markup(_(f"Showing <b>{count}</b> results"))
+            self.search_result_label.set_markup(_(f"Showing <b>{count}</b> results"))
         else:
-            self.result_label.set_markup(_(f"<b>{total_count}</b> results, only displaying first {count}"))
-        for row in self.listbox.get_children():
+            self.search_result_label.set_markup(_(f"<b>{total_count}</b> results, only displaying first {count}"))
+        for row in self.search_listbox.get_children():
             row.destroy()
         for game in api_games.get("results", []):
             platforms = ",".join(gtk_safe(platform["name"]) for platform in game["platforms"])
@@ -242,31 +278,205 @@ def update_search_results_cb(self, api_games, error):
             if platforms and year:
                 platforms = ", " + platforms
 
-            row = self.build_row("", gtk_safe(game['name']), f"{year}{platforms}")
+            row = self._get_listbox_row("", gtk_safe(game['name']), f"{year}{platforms}")
             row.api_info = game
-            self.listbox.add(row)
-        self.listbox.show()
+            self.search_listbox.add(row)
+        self.search_result_label.show()
+        self.search_frame.show()
+        self.search_explanation_label.hide()
+
+    @watch_errors()
+    def _on_game_selected(self, listbox, row):
+        game_slug = row.api_info["slug"]
+        installers = get_installers(game_slug=game_slug)
+        application = Gio.Application.get_default()
+        application.show_installer_window(installers)
+        self.destroy()
+
+    # Scan Folder Page
+
+    def scan_folder(self):
+        """Scan a folder of already installed games"""
+        self.stack.navigate_to_page(self.present_scan_folder_page)
+
+    def create_scan_folder_page(self):
+        grid = Gtk.Grid(row_spacing=6, column_spacing=6)
+        label = self._get_label(_("Folder to scan"))
+        grid.attach(label, 0, 0, 1, 1)
+        grid.attach(self.scan_directory_chooser, 1, 0, 1, 1)
+        self.scan_directory_chooser.set_hexpand(True)
+
+        explanation = _(
+            "Lutris will search this folder for sub-folders that contain games it recognizes.\n\n"
+            "Any games it finds that are not already in Lutris will be added.\n\n"
+            "When you click 'Continue' below, the search will begin, and any games found will "
+            "be added at once."
+        )
+
+        grid.attach(self._get_explanation_label(explanation), 0, 1, 2, 1)
+        return grid
+
+    def present_scan_folder_page(self):
+        self.title_label.set_markup("<b>Select folder to scan for games</b>")
+        self.stack.present_page("scan_folder")
+        self.display_continue_button(self.on_continue_scan_folder_clicked)
+
+    def on_continue_scan_folder_clicked(self, _widget):
+        path = self.scan_directory_chooser.get_text()
+        if not path:
+            ErrorDialog(_("You must select a folder to scan for games."), parent=self)
+        elif not os.path.isdir(path):
+            ErrorDialog(_("No folder exists at '%s'.") % path, parent=self)
+        else:
+            self.load_scanning_folder_page(path)
+
+    # Scanning Folder Page
+
+    def load_scanning_folder_page(self, path):
+        def present_scanning_folder_page():
+            self.title_label.set_markup("<b>Importing games from a folder</b>")
+            self.stack.present_page("scanning_folder")
+            self.display_no_continue_button()
+            AsyncCall(scan_directory, self._on_folder_scanned, path)
+
+        self.stack.jump_to_page(present_scanning_folder_page)
+
+    def create_scanning_folder_page(self):
+        spinner = Gtk.Spinner()
+        spinner.start()
+        return spinner
+
+    @watch_errors()
+    def _on_folder_scanned(self, result, error):
+        def present_installed_games_page():
+            if installed or missing:
+                self.title_label.set_markup(_("<b>Games found</b>"))
+            else:
+                self.title_label.set_markup(_("<b>No games found</b>"))
+
+            page = self.create_installed_games_page(installed, missing)
+            self.stack.present_replacement_page("installed_games", page)
+            self.display_cancel_button(label=_("_Close"))
+
+        if error:
+            ErrorDialog(str(error), parent=self)
+            self.stack.navigation_reset()
+            return
+
+        installed, missing = result
+        self.stack.navigate_to_page(present_installed_games_page)
+
+    def create_installed_games_page(self, installed, missing):
+        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
+
+        if installed:
+            installed_label = self._get_label("Installed games")
+            vbox.pack_start(installed_label, False, False, 0)
+
+            installed_listbox = Gtk.ListBox()
+            installed_scroll = Gtk.ScrolledWindow(shadow_type=Gtk.ShadowType.ETCHED_IN)
+            installed_scroll.set_vexpand(True)
+            installed_scroll.add(installed_listbox)
+            vbox.pack_start(installed_scroll, True, True, 0)
+            for folder in installed:
+                installed_listbox.add(self._get_listbox_row("", gtk_safe(folder), ""))
+
+        if missing:
+            missing_listbox = Gtk.ListBox()
+            missing_scroll = Gtk.ScrolledWindow(shadow_type=Gtk.ShadowType.ETCHED_IN)
+            missing_scroll.set_vexpand(True)
+            missing_scroll.add(missing_listbox)
+            vbox.pack_end(missing_scroll, True, True, 0)
+            for folder in missing:
+                missing_listbox.add(self._get_listbox_row("", gtk_safe(folder), ""))
+
+            missing_label = self._get_label("No match found")
+            vbox.pack_end(missing_label, False, False, 0)
+
+        return vbox
+
+    # Install from Setup Page
 
     def install_from_setup(self):
         """Install from a setup file"""
+        self.stack.navigate_to_page(self.present_install_from_setup_page)
+
+    def create_install_from_setup_page(self):
+        name_label = self._get_label(_("Game name"))
+
+        self.install_from_setup_game_name_entry.set_hexpand(True)
+        self.install_from_setup_game_slug_entry.set_hexpand(True)
+
+        grid = Gtk.Grid(row_spacing=6, column_spacing=6)
+        grid.set_column_homogeneous(False)
+        grid.attach(name_label, 0, 0, 1, 1)
+        grid.attach(self.install_from_setup_game_name_entry, 1, 0, 1, 1)
+        grid.attach(self.install_from_setup_game_slug_checkbox, 0, 1, 1, 1)
+        grid.attach(self.install_from_setup_game_slug_entry, 1, 1, 1, 1)
+
+        self.install_from_setup_game_name_entry.connect("changed", self.on_install_from_setup_game_name_changed)
+        self.install_from_setup_game_slug_checkbox.connect("toggled", self.on_install_from_setup_game_slug_toggled)
+
+        explanation = _(
+            "Enter the name of the game you will install.\n\nWhen you click 'Install' below, "
+            "the installer window will appear and guide you through a simple installation.\n\n"
+            "It will prompt you for a setup executable, and will use Wine to install it.\n\n"
+            "If you know the Lutris identifier for the game, you can provide it for improved "
+            "Lutris integration, such as Lutris provided banners."
+        )
+
+        grid.attach(self._get_explanation_label(explanation), 0, 2, 2, 1)
+
+        if LINUX_SYSTEM.is_64_bit:
+            self.install_from_setup_32bit_prefix_checkbox = Gtk.CheckButton(
+                label=_("32-bit Wine prefix"))
+
+            grid.attach(self.install_from_setup_32bit_prefix_checkbox, 0, 3, 2, 1)
+            self.install_from_setup_32bit_prefix_checkbox.set_valign(Gtk.Align.END)
+            self.install_from_setup_32bit_prefix_checkbox.set_vexpand(True)
+        grid.set_vexpand(True)
+        return grid
+
+    def present_install_from_setup_page(self):
         self.title_label.set_markup(_("<b>Select setup file</b>"))
-        self.listbox.destroy()
-        label = self._get_label(_("Game name"))
-        self.vbox.add(label)
-        entry = Gtk.Entry(visible=True)
-        self.vbox.add(entry)
-        button = Gtk.Button(_("Continue"), visible=True)
-        button.connect("clicked", self._on_install_setup_continue, entry)
-        self.vbox.add(button)
+        self.stack.present_page("install_from_setup")
+        self.display_continue_button(self._on_install_setup_continue, label=_("_Install"))
 
     @watch_errors()
-    def _on_install_setup_continue(self, button, entry):
-        name = entry.get_text().strip()
+    def on_install_from_setup_game_slug_toggled(self, checkbutton):
+        self.install_from_setup_game_slug_entry.set_sensitive(checkbutton.get_active())
+        self.on_install_from_setup_game_name_changed()
+
+    @watch_errors()
+    def on_install_from_setup_game_name_changed(self, *_args):
+        if not self.install_from_setup_game_slug_checkbox.get_active():
+            name = self.install_from_setup_game_name_entry.get_text()
+            self.install_from_setup_game_slug_entry.set_text(slugify(name))
+
+    @watch_errors()
+    def _on_install_setup_continue(self, button):
+        name = self.install_from_setup_game_name_entry.get_text().strip()
+
+        if not name:
+            ErrorDialog(_("You must provide a name for the game you are installing."), parent=self)
+            return
+
+        if self.install_from_setup_game_slug_checkbox.get_active():
+            game_slug = self.install_from_setup_game_slug_entry.get_text()
+        else:
+            game_slug = slugify(name)
+
+        if (self.install_from_setup_32bit_prefix_checkbox
+                and not self.install_from_setup_32bit_prefix_checkbox.get_active()):
+            arch = "win64"
+        else:
+            arch = "win32"
+
         installer = {
             "name": name,
             "version": _("Setup file"),
-            "slug": slugify(name) + "-setup",
-            "game_slug": slugify(name),
+            "slug": game_slug + "-setup",
+            "game_slug": game_slug,
             "runner": "wine",
             "script": {
                 "game": {
@@ -276,7 +486,7 @@ def _on_install_setup_continue(self, button, entry):
                     {"setupfile": "N/A:%s" % _("Select the setup file")}
                 ],
                 "installer": [
-                    {"task": {"name": "wineexec", "executable": "setupfile"}}
+                    {"task": {"name": "wineexec", "executable": "setupfile", "arch": arch}}
                 ]
             }
         }
@@ -284,16 +494,176 @@ def _on_install_setup_continue(self, button, entry):
         application.show_installer_window([installer])
         self.destroy()
 
+    # Install from Script Page
+
     def install_from_script(self):
         """Install from a YAML file"""
-        script_dlg = FileDialog(_("Select a Lutris installer"))
-        if script_dlg.filename:
-            installers = get_installers(installer_file=script_dlg.filename)
+        self.stack.navigate_to_page(self.present_install_from_script_page)
+
+    def create_install_from_script_page(self):
+        grid = Gtk.Grid(row_spacing=6, column_spacing=6)
+        label = self._get_label(_("Script file"))
+        grid.attach(label, 0, 0, 1, 1)
+        grid.attach(self.install_script_file_chooser, 1, 0, 1, 1)
+        self.install_script_file_chooser.set_hexpand(True)
+
+        explanation = _(
+            "Lutris install scripts are YAML files that guide Lutris through "
+            "the installation process.\n\n"
+            "They can be obtained on Lutris.net, or written by hand.\n\n"
+            "When you click 'Install' below, the installer window will "
+            "appear and load the script, and it will guide the process from there."
+        )
+
+        grid.attach(self._get_explanation_label(explanation), 0, 1, 2, 1)
+        return grid
+
+    def present_install_from_script_page(self):
+        self.title_label.set_markup("<b>Select a Lutris installer</b>")
+        self.stack.present_page("install_from_script")
+        self.display_continue_button(self.on_continue_install_from_script_clicked, label=_("_Install"))
+
+    def on_continue_install_from_script_clicked(self, _widget):
+        path = self.install_script_file_chooser.get_text()
+        if not path:
+            ErrorDialog(_("You must select a script file to install."), parent=self)
+        elif not os.path.isfile(path):
+            ErrorDialog(_("No file exists at '%s'.") % path, parent=self)
+        else:
+            installers = get_installers(installer_file=path)
             application = Gio.Application.get_default()
             application.show_installer_window(installers)
-        self.destroy()
+            self.destroy()
+
+    # Install ROM Page
+
+    def import_rom(self):
+        """Install from a YAML file"""
+        self.stack.navigate_to_page(self.present_import_rom_page)
+
+    def create_import_rom_page(self):
+        grid = Gtk.Grid(row_spacing=6, column_spacing=6)
+        label = self._get_label(_("ROM file"))
+        grid.attach(label, 0, 0, 1, 1)
+        grid.attach(self.import_rom_file_chooser, 1, 0, 1, 1)
+        self.import_rom_file_chooser.set_hexpand(True)
+
+        explanation = _(
+            "Lutris will identify a ROM via its MD5 hash and download game "
+            "information from Lutris.net.\n\n"
+            "The ROM data used for this comes from the TOSEC project.\n\n"
+            "When you click 'Install' below, the process of installing the game will "
+            "begin."
+        )
+
+        grid.attach(self._get_explanation_label(explanation), 0, 1, 2, 1)
+        return grid
+
+    def present_import_rom_page(self):
+        self.title_label.set_markup("<b>Select a ROM file</b>")
+        self.stack.present_page("import_rom")
+        self.display_continue_button(self.on_continue_import_rom_clicked, label=_("_Install"))
+
+    def on_continue_import_rom_clicked(self, _widget):
+        path = self.import_rom_file_chooser.get_text()
+        if not path:
+            ErrorDialog(_("You must select a ROM file to install."), parent=self)
+        elif not os.path.isfile(path):
+            ErrorDialog(_("No file exists at '%s'.") % path, parent=self)
+        else:
+            dialog = ImportGameDialog([path], parent=self)
+            dialog.run()
+            dialog.destroy()
+            self.destroy()
+
+    # Add Local Game
 
     def add_local_game(self):
         """Manually configure game"""
-        AddGameDialog(None)
-        self.destroy()
+        AddGameDialog(parent=self)
+        GLib.idle_add(self.destroy)  # defer destory so the game dialog can be centered first
+
+    # Continue Button
+
+    def display_continue_button(self, handler, label=_("_Continue"), suggested_action=True):
+        self.continue_button.set_label(label)
+        style_context = self.continue_button.get_style_context()
+
+        if suggested_action:
+            style_context.add_class("suggested-action")
+        else:
+            style_context.remove_class("suggested-action")
+
+        if self.continue_handler:
+            self.continue_button.disconnect(self.continue_handler)
+
+        self.continue_handler = self.continue_button.connect("clicked", handler)
+
+        self.continue_button.show()
+        self.cancel_button.set_label(_("Cancel"))
+        self.cancel_button.show()
+
+    def display_cancel_button(self, label=_("Cancel")):
+        self.cancel_button.set_label(label)
+        self.cancel_button.show()
+        self.continue_button.hide()
+
+    def display_no_continue_button(self):
+        self.continue_button.hide()
+        self.cancel_button.hide()
+
+        if self.continue_handler:
+            self.continue_button.disconnect(self.continue_handler)
+            self.continue_handler = None
+
+    # Implementation
+
+    def _get_icon(self, name, small=False):
+        if small:
+            size = Gtk.IconSize.MENU
+        else:
+            size = Gtk.IconSize.DND
+        icon = Gtk.Image.new_from_icon_name(name, size)
+        icon.show()
+        return icon
+
+    def _get_label(self, text):
+        label = Gtk.Label(visible=True)
+        label.set_markup(text)
+        label.set_alignment(0, 0.5)
+        return label
+
+    def _get_explanation_label(self, markup):
+        label = Gtk.Label(
+            visible=True,
+            margin_right=12,
+            margin_left=12,
+            margin_top=12,
+            margin_bottom=12)
+        label.set_markup(markup)
+        label.set_line_wrap(True)
+        return label
+
+    def _get_listbox_row(self, left_icon_name, text, subtext, right_icon_name=""):
+        row = Gtk.ListBoxRow(visible=True)
+        row.set_selectable(False)
+        row.set_activatable(True)
+
+        box = Gtk.Box(
+            spacing=12,
+            margin_right=12,
+            margin_left=12,
+            margin_top=12,
+            margin_bottom=12,
+            visible=True)
+
+        if left_icon_name:
+            icon = self._get_icon(left_icon_name)
+            box.pack_start(icon, False, False, 0)
+        label = self._get_label(f"<b>{text}</b>\n{subtext}")
+        box.pack_start(label, True, True, 0)
+        if left_icon_name:
+            next_icon = self._get_icon(right_icon_name, small=True)
+            box.pack_start(next_icon, False, False, 0)
+        row.add(box)
+        return row
diff --git a/lutris/gui/installerwindow.py b/lutris/gui/installerwindow.py
--- a/lutris/gui/installerwindow.py
+++ b/lutris/gui/installerwindow.py
@@ -15,6 +15,7 @@
 from lutris.gui.installer.script_picker import InstallerPicker
 from lutris.gui.widgets.common import FileChooserEntry
 from lutris.gui.widgets.log_text_view import LogTextView
+from lutris.gui.widgets.navigation_stack import NavigationStack
 from lutris.gui.widgets.window import BaseApplicationWindow
 from lutris.installer import InstallationKind, get_installers, interpreter
 from lutris.installer.errors import MissingGameDependency, ScriptingError
@@ -76,6 +77,10 @@ def __init__(
         # Action buttons
 
         self.back_button = self.add_start_button(_("Back"), self.on_back_clicked, sensitive=False)
+        key, mod = Gtk.accelerator_parse("<Alt>Left")
+        self.back_button.add_accelerator("clicked", self.accelerators, key, mod, Gtk.AccelFlags.VISIBLE)
+        key, mod = Gtk.accelerator_parse("<Alt>Home")
+        self.accelerators.connect(key, mod, Gtk.AccelFlags.VISIBLE, self.on_navigate_home)
         self.cache_button = self.add_start_button(_("Cache"), self.on_cache_clicked,
                                                   tooltip=_("Change where Lutris downloads game installer files."))
 
@@ -90,7 +95,7 @@ def __init__(
 
         # Navigation stack
 
-        self.stack = InstallerWindow.NavigationStack(self.back_button)
+        self.stack = NavigationStack(self.back_button)
         self.register_page_creators()
         self.vbox.pack_start(self.stack, True, True, 0)
 
@@ -157,6 +162,10 @@ def on_cache_clicked(self, _button):
     def on_back_clicked(self, _button):
         self.stack.navigate_back()
 
+    @watch_errors()
+    def on_navigate_home(self, _accel_group, _window, _keyval, _modifier):
+        self.stack.navigate_home()
+
     def on_destroy(self, _widget, _data=None):
         self.on_cancel_clicked()
 
@@ -935,157 +944,3 @@ def __init__(self, markup=None, selectable=True):
                 max_width_chars=80,
                 selectable=selectable)
             self.set_alignment(0.5, 0)
-
-    class NavigationStack(Gtk.Stack):
-        """
-        This is a Stack widget that supports a back button and
-        lazy-creation of pages.
-
-        Pages should be set up via add_named_factory(), then displayed
-        with present_page().
-
-        However, you are meant to have 'present_X_page' functions
-        that you pass to navigate_to_page(); this tracks the pages
-        you visit, and when you navigate back,the presenter function
-        will be called again.
-
-        A presenter function can do more than just call present_page();
-        it can configure other aspects of the InstallerWindow. Packaging
-        all this into a presenter function keeps things in sync as you navigate.
-
-        A present function can return an exit function, called when you navigate away
-        from the page again.
-        """
-
-        def __init__(self, back_button, **kwargs):
-            super().__init__(**kwargs)
-
-            self.back_button = back_button
-            self.page_factories = {}
-            self.stack_pages = {}
-            self.navigation_stack = []
-            self.navigation_exit_hander = None
-            self.current_page_presenter = None
-            self.current_navigated_page_presenter = None
-            self.back_allowed = True
-
-        def add_named_factory(self, name, factory):
-            """This specifies the factory functioin for the page named;
-            this function takes no arguments, but returns the page's widget."""
-            self.page_factories[name] = factory
-
-        def set_back_allowed(self, is_allowed=True):
-            """This turns the back button off, or back on."""
-            self.back_allowed = is_allowed
-            self._update_back_button()
-
-        def _update_back_button(self):
-            self.back_button.set_sensitive(self.back_allowed and self.navigation_stack)
-
-        def navigate_to_page(self, page_presenter):
-            """Navigates to a page, by invoking 'page_presenter'.
-
-            In addition, this updates the navigation state so navigate_back()
-            and such work, they may call the presenter again.
-            """
-            if self.current_navigated_page_presenter:
-                self.navigation_stack.append(self.current_navigated_page_presenter)
-                self._update_back_button()
-
-            self._go_to_page(page_presenter, True, Gtk.StackTransitionType.SLIDE_LEFT)
-
-        def jump_to_page(self, page_presenter):
-            """Jumps to a page, without updating navigation state.
-
-            This does not disturb the behavior of navigate_back().
-            This does invoke the exit handler of the current page.
-            """
-            self._go_to_page(page_presenter, False, Gtk.StackTransitionType.NONE)
-
-        def navigate_back(self):
-            """This navigates to the previous page, if any. This will invoke the
-            current page's exit function, and the previous page's presenter function.
-            """
-            if self.navigation_stack:
-                try:
-                    back_to = self.navigation_stack.pop()
-                    self._go_to_page(back_to, True, Gtk.StackTransitionType.SLIDE_RIGHT)
-                finally:
-                    self._update_back_button()
-
-        def navigation_reset(self):
-            """This reverse the effect of jump_to_page(), returning to the last
-            page actually navigate to."""
-            if self.current_navigated_page_presenter:
-                if self.current_page_presenter != self.current_navigated_page_presenter:
-                    self._go_to_page(self.current_navigated_page_presenter, True, Gtk.StackTransitionType.SLIDE_RIGHT)
-
-        def save_current_page(self):
-            """Returns a tuple containing information about the current page,
-            to pass to restore_current_page()."""
-            return (self.current_page_presenter, self.current_navigated_page_presenter)
-
-        def restore_current_page(self, state):
-            """Restores the current page to the one in effect when the state was generated.
-            This does not disturb the navigation stack."""
-            page_presenter, navigated_presenter = state
-            navigated = page_presenter == navigated_presenter
-            self._go_to_page(page_presenter, navigated, Gtk.StackTransitionType.NONE)
-
-        def _go_to_page(self, page_presenter, navigated, transition_type):
-            """Switches to a page. If 'navigated' is True, then when you navigate
-            away from this page, it can go on the navigation stack. It should be
-            False for 'temporary' pages that are not part of normal navigation."""
-            exit_handler = self.navigation_exit_hander
-            self.set_transition_type(transition_type)
-            self.navigation_exit_hander = page_presenter()
-            self.current_page_presenter = page_presenter
-            if navigated:
-                self.current_navigated_page_presenter = page_presenter
-            if exit_handler:
-                exit_handler()
-
-        def discard_navigation(self):
-            """This throws away the navigation history, so the back
-            button is disabled. Previous pages before the current become
-            inaccessible."""
-            self.navigation_stack.clear()
-            self._update_back_button()
-
-        def present_page(self, name):
-            """This displays the page names, creating it if required. It
-            also calls show_all() on newly created pages.
-
-            This should be called by your presenter functions."""
-            if name not in self.stack_pages:
-                factory = self.page_factories[name]
-                page = factory()
-                page.show_all()
-
-                self.add_named(page, name)
-                self.stack_pages[name] = page
-
-            self.set_visible_child_name(name)
-            return self.stack_pages[name]
-
-        def present_replacement_page(self, name, page):
-            """This displays a page that is given, rather than lazy-creating one. It
-            still needs a name, but if you re-use a name this will replace the old page.
-
-            This is useful for pages that need special initialization each time they
-            appear, but generally such pages can't be returned to via the back
-            button. The caller must protect against this if required.
-            """
-            old_page = self.stack_pages.get(name)
-
-            if old_page != page:
-                if old_page:
-                    self.remove(old_page)
-
-                page.show_all()
-
-                self.add_named(page, name)
-                self.stack_pages[name] = page
-
-            self.set_visible_child_name(name)
-            return page
diff --git a/lutris/gui/widgets/navigation_stack.py b/lutris/gui/widgets/navigation_stack.py
new file mode 100644
--- /dev/null
+++ b/lutris/gui/widgets/navigation_stack.py
@@ -0,0 +1,170 @@
+"""Window used for game installers"""
+# pylint: disable=too-many-lines
+from gi.repository import Gtk
+
+
+class NavigationStack(Gtk.Stack):
+    """
+    This is a Stack widget that supports a back button and
+    lazy-creation of pages.
+
+    Pages should be set up via add_named_factory(), then displayed
+    with present_page().
+
+    However, you are meant to have 'present_X_page' functions
+    that you pass to navigate_to_page(); this tracks the pages
+    you visit, and when you navigate back,the presenter function
+    will be called again.
+
+    A presenter function can do more than just call present_page();
+    it can configure other aspects of the InstallerWindow. Packaging
+    all this into a presenter function keeps things in sync as you navigate.
+
+    A presenter function can return an exit function, called when you navigate away
+    from the page again.
+    """
+
+    def __init__(self, back_button, **kwargs):
+        super().__init__(**kwargs)
+
+        self.back_button = back_button
+        self.page_factories = {}
+        self.stack_pages = {}
+        self.navigation_stack = []
+        self.navigation_exit_hander = None
+        self.current_page_presenter = None
+        self.current_navigated_page_presenter = None
+        self.back_allowed = True
+
+    def add_named_factory(self, name, factory):
+        """This specifies the factory functioin for the page named;
+        this function takes no arguments, but returns the page's widget."""
+        self.page_factories[name] = factory
+
+    def set_back_allowed(self, is_allowed=True):
+        """This turns the back button off, or back on."""
+        self.back_allowed = is_allowed
+        self._update_back_button()
+
+    def _update_back_button(self):
+        self.back_button.set_sensitive(self.back_allowed and self.navigation_stack)
+
+    def navigate_to_page(self, page_presenter):
+        """Navigates to a page, by invoking 'page_presenter'.
+
+        In addition, this updates the navigation state so navigate_back()
+        and such work, they may call the presenter again.
+        """
+        if self.current_navigated_page_presenter:
+            self.navigation_stack.append(self.current_navigated_page_presenter)
+            self._update_back_button()
+
+        self._go_to_page(page_presenter, True, Gtk.StackTransitionType.SLIDE_LEFT)
+
+    def jump_to_page(self, page_presenter):
+        """Jumps to a page, without updating navigation state.
+
+        This does not disturb the behavior of navigate_back().
+        This does invoke the exit handler of the current page.
+        """
+        self._go_to_page(page_presenter, False, Gtk.StackTransitionType.NONE)
+
+    def navigate_back(self):
+        """This navigates to the previous page, if any. This will invoke the
+        current page's exit function, and the previous page's presenter function.
+        """
+        if self.navigation_stack and self.back_allowed:
+            try:
+                back_to = self.navigation_stack.pop()
+                self._go_to_page(back_to, True, Gtk.StackTransitionType.SLIDE_RIGHT)
+            finally:
+                self._update_back_button()
+
+    def navigate_home(self):
+        """This navigates to the first page, effectively navigating back until it
+        can go no further back. It does not actually traverse the intermediate pages
+        though, but goes directly to the first."""
+        if self.navigation_stack and self.back_allowed:
+            try:
+                home = self.navigation_stack[0]
+                self.navigation_stack.clear()
+                self._go_to_page(home, True, Gtk.StackTransitionType.SLIDE_RIGHT)
+            finally:
+                self._update_back_button()
+
+    def navigation_reset(self):
+        """This reverse the effect of jump_to_page(), returning to the last
+        page actually navigate to."""
+        if self.current_navigated_page_presenter:
+            if self.current_page_presenter != self.current_navigated_page_presenter:
+                self._go_to_page(self.current_navigated_page_presenter, True, Gtk.StackTransitionType.SLIDE_RIGHT)
+
+    def save_current_page(self):
+        """Returns a tuple containing information about the current page,
+        to pass to restore_current_page()."""
+        return (self.current_page_presenter, self.current_navigated_page_presenter)
+
+    def restore_current_page(self, state):
+        """Restores the current page to the one in effect when the state was generated.
+        This does not disturb the navigation stack."""
+        page_presenter, navigated_presenter = state
+        navigated = page_presenter == navigated_presenter
+        self._go_to_page(page_presenter, navigated, Gtk.StackTransitionType.NONE)
+
+    def _go_to_page(self, page_presenter, navigated, transition_type):
+        """Switches to a page. If 'navigated' is True, then when you navigate
+        away from this page, it can go on the navigation stack. It should be
+        False for 'temporary' pages that are not part of normal navigation."""
+        exit_handler = self.navigation_exit_hander
+        self.set_transition_type(transition_type)
+        self.navigation_exit_hander = page_presenter()
+        self.current_page_presenter = page_presenter
+        if navigated:
+            self.current_navigated_page_presenter = page_presenter
+        if exit_handler:
+            exit_handler()
+
+    def discard_navigation(self):
+        """This throws away the navigation history, so the back
+        button is disabled. Previous pages before the current become
+        inaccessible."""
+        self.navigation_stack.clear()
+        self._update_back_button()
+
+    def present_page(self, name):
+        """This displays the page names, creating it if required. It
+        also calls show_all() on newly created pages.
+
+        This should be called by your presenter functions."""
+        if name not in self.stack_pages:
+            factory = self.page_factories[name]
+            page = factory()
+            page.show_all()
+
+            self.add_named(page, name)
+            self.stack_pages[name] = page
+
+        self.set_visible_child_name(name)
+        return self.stack_pages[name]
+
+    def present_replacement_page(self, name, page):
+        """This displays a page that is given, rather than lazy-creating one. It
+        still needs a name, but if you re-use a name this will replace the old page.
+
+        This is useful for pages that need special initialization each time they
+        appear, but generally such pages can't be returned to via the back
+        button. The caller must protect against this if required.
+        """
+        old_page = self.stack_pages.get(name)
+
+        if old_page != page:
+            if old_page:
+                self.remove(old_page)
+
+            page.show_all()
+
+            self.add_named(page, name)
+            self.stack_pages[name] = page
+
+        self.set_visible_child_name(name)
+        return page


Test patch: