[gnome-boxes/boxes-newbox-assistant-306: 7/7] assistant: Introduce the new Assistant/Download manager



commit ef9458ec49a39d84cfa53a90fa86d3925f98f25d
Author: Felipe Borges <felipeborges gnome org>
Date:   Mon Nov 4 10:00:33 2019 +0100

    assistant: Introduce the new Assistant/Download manager
    
    The new VM Assistant is a brand new implementation of the virtual
    machine creation dialog. The purpose of this rewrite was to decouple
    the assistant logic from the rest of the application.
    
    The dialog implementation is described in detail at
    https://gitlab.gnome.org/GNOME/gnome-boxes/wikis/source-overview/Virtual-Machine-Assistant
    
    Fixes #306
    Fixes #410
    Fixes #409

 data/gnome-boxes.gresource.xml              |   9 +
 data/gtk-style.css                          |  18 +-
 data/ui/assistant/pages/downloads-page.ui   | 139 +++++++++++++
 data/ui/assistant/pages/index-page.ui       | 305 ++++++++++++++++++++++++++++
 data/ui/assistant/pages/preparation-page.ui |  84 ++++++++
 data/ui/assistant/pages/review-page.ui      | 108 ++++++++++
 data/ui/assistant/pages/setup-page.ui       |  21 ++
 data/ui/assistant/rhel-download-dialog.ui   |  45 ++++
 data/ui/assistant/vm-assistant.ui           |  65 ++++++
 data/ui/collection-toolbar.ui               |  29 ++-
 data/ui/downloads-hub-row.ui                |  70 +++++++
 data/ui/downloads-hub.ui                    |  16 ++
 data/ui/wizard-downloadable-entry.ui        |   3 -
 data/ui/wizard-media-entry.ui               |   5 +-
 src/app-window.vala                         |   6 +-
 src/app.vala                                |  10 +-
 src/assistant/assistant-page.vala           |  17 ++
 src/assistant/downloads-page.vala           | 118 +++++++++++
 src/assistant/index-page.vala               | 159 +++++++++++++++
 src/assistant/preparation-page.vala         |  69 +++++++
 src/assistant/review-page.vala              | 123 +++++++++++
 src/assistant/rhel-download-dialog.vala     | 182 +++++++++++++++++
 src/assistant/setup-page.vala               |  34 ++++
 src/assistant/vm-assistant.vala             | 149 ++++++++++++++
 src/collection-toolbar.vala                 |  12 +-
 src/downloads-hub.vala                      | 129 ++++++++++++
 src/meson.build                             |   9 +
 src/util-app.vala                           |  16 ++
 src/vm-creator.vala                         |   2 +-
 29 files changed, 1935 insertions(+), 17 deletions(-)
---
diff --git a/data/gnome-boxes.gresource.xml b/data/gnome-boxes.gresource.xml
index c7ba7de1..4db4ad80 100644
--- a/data/gnome-boxes.gresource.xml
+++ b/data/gnome-boxes.gresource.xml
@@ -13,6 +13,8 @@
     <file preprocess="xml-stripblanks">ui/collection-toolbar.ui</file>
     <file preprocess="xml-stripblanks">ui/display-page.ui</file>
     <file preprocess="xml-stripblanks">ui/display-toolbar.ui</file>
+    <file preprocess="xml-stripblanks">ui/downloads-hub.ui</file>
+    <file preprocess="xml-stripblanks">ui/downloads-hub-row.ui</file>
     <file preprocess="xml-stripblanks">ui/editable-entry.ui</file>
     <file preprocess="xml-stripblanks">ui/empty-boxes.ui</file>
     <file preprocess="xml-stripblanks">ui/icon-view.ui</file>
@@ -48,5 +50,12 @@
     <file preprocess="xml-stripblanks">ui/wizard-web-view.ui</file>
     <file preprocess="xml-stripblanks">ui/wizard-window.ui</file>
     <file preprocess="xml-stripblanks">ui/assistant/remote-connection.ui</file>
+    <file preprocess="xml-stripblanks">ui/assistant/vm-assistant.ui</file>
+    <file preprocess="xml-stripblanks">ui/assistant/rhel-download-dialog.ui</file>
+    <file preprocess="xml-stripblanks">ui/assistant/pages/index-page.ui</file>
+    <file preprocess="xml-stripblanks">ui/assistant/pages/downloads-page.ui</file>
+    <file preprocess="xml-stripblanks">ui/assistant/pages/preparation-page.ui</file>
+    <file preprocess="xml-stripblanks">ui/assistant/pages/setup-page.ui</file>
+    <file preprocess="xml-stripblanks">ui/assistant/pages/review-page.ui</file>
   </gresource>
 </gresources>
diff --git a/data/gtk-style.css b/data/gtk-style.css
index 40f0391d..622f14d4 100644
--- a/data/gtk-style.css
+++ b/data/gtk-style.css
@@ -43,6 +43,10 @@
     border: 1px solid @theme_bg_color;
 }
 
+.bold-label {
+    font-weight: bold;
+}
+
 /******************
  * New Box Wizard *
  ******************/
@@ -81,18 +85,24 @@
 }
 
 .boxes-menu-row {
-    background: none;
-    border: 1px solid @borders;
+    background: @theme_unfocused_base_color;
 }
 
 .boxes-menu-row:hover {
     background-color: @theme_bg_color;
 }
 
-.boxes-menu-subrow {
-    padding: 10px;
+.sources-list {
+    border-radius: 5px;
 }
 
+separator {
+    background-color: @borders;
+}
+
+.list-expand-button {
+    border-top: 1px solid @borders;
+}
 
 /* Adds a border to the ISOs lists top undershoot */
 .boxes-menu-scrolled.undershoot.top { border-top: 1px solid @borders; }
diff --git a/data/ui/assistant/pages/downloads-page.ui b/data/ui/assistant/pages/downloads-page.ui
new file mode 100644
index 00000000..2219a4ac
--- /dev/null
+++ b/data/ui/assistant/pages/downloads-page.ui
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.9 -->
+  <template class="BoxesAssistantDownloadsPage" parent="GtkStack">
+    <signal name="key-press-event" handler="on_key_pressed"/>
+
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <property name="border-width">20</property>
+        <property name="margin-start">20</property>
+        <property name="margin-end">20</property>
+        <style>
+          <class name="linked"/>
+          <class name="frame"/>
+          <class name="sources-list"/>
+        </style>
+
+        <child>
+          <object class="GtkListBox" id="recommended_listbox">
+            <property name="visible">True</property>
+            <property name="vexpand">True</property>
+            <property name="selection-mode">none</property>
+            <signal name="row-activated" handler="on_listbox_row_activated"/>
+          </object>
+        </child>
+
+        <child>
+          <object class="GtkButton" id="show_more_button">
+            <property name="visible">True</property>
+            <signal name="clicked" handler="on_show_more_button_clicked"/>
+            <style>
+              <class name="list-expand-button"/>
+              <class name="boxes-menu-row"/>
+              <class name="flat"/>
+            </style>
+
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="icon-name">view-more-symbolic</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="name">recommended</property>
+      </packing>
+    </child>
+
+    <child>
+      <object class="GtkScrolledWindow">
+        <property name="visible">True</property>
+        <property name="expand">True</property>
+
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="orientation">vertical</property>
+            <property name="border-width">20</property>
+            <property name="margin-start">20</property>
+            <property name="margin-end">20</property>
+
+            <child>
+              <object class="GtkListBox" id="listbox">
+                <property name="visible">True</property>
+                <property name="selection-mode">none</property>
+                <signal name="row-activated" handler="on_listbox_row_activated"/>
+                <style>
+                  <class name="sources-list"/>
+                  <class name="frame"/>
+                </style>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="name">search-results</property>
+      </packing>
+    </child>
+
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <property name="opacity">0.5</property>
+        <property name="spacing">10</property>
+        <property name="valign">center</property>
+        <style>
+          <class name="dim-label"/>
+        </style>
+
+        <child>
+          <object class="GtkImage">
+            <property name="visible">True</property>
+            <property name="resource">/org/gnome/Boxes/icons/empty-boxes.png</property>
+            <property name="pixel-size">128</property>
+            <property name="halign">center</property>
+            <property name="valign">center</property>
+            <property name="margin">18</property>
+          </object>
+        </child>
+
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">No operating systems found</property>
+            <attributes>
+              <attribute name="scale" value="2"/>
+              <attribute name="weight" value="bold"/>
+            </attributes>
+          </object>
+        </child>
+
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">Try a different search</property>
+          </object>
+        </child>
+
+      </object>
+      <packing>
+        <property name="name">no-results</property>
+      </packing>
+    </child>
+
+  </template>
+
+       <object class="GtkSearchEntry" id="searchbar">
+            <property name="visible">True</property>
+            <property name="width-chars">50</property>
+            <property name="can-focus">True</property>
+            <property name="placeholder-text" translatable="yes">Search for an OS or enter a download 
link…</property>
+          </object>
+</interface>
diff --git a/data/ui/assistant/pages/index-page.ui b/data/ui/assistant/pages/index-page.ui
new file mode 100644
index 00000000..6eb98db7
--- /dev/null
+++ b/data/ui/assistant/pages/index-page.ui
@@ -0,0 +1,305 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="BoxesAssistantIndexPage" parent="BoxesAssistantPage">
+    <property name="visible">True</property>
+    <property name="title" translatable="yes">Create a Virtual Machine</property>
+
+    <child>
+      <object class="GtkStack" id="stack">
+        <property name="visible">True</property>
+        <signal name="notify::visible-child" handler="update_topbar"/>
+
+        <child>
+          <object class="GtkScrolledWindow" id="home_page">
+            <property name="visible">True</property>
+            <property name="expand">True</property>
+            <property name="hscrollbar-policy">never</property>
+
+            <child>
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <property name="orientation">vertical</property>
+                <property name="spacing">30</property>
+                <property name="halign">center</property>
+                <property name="border-width">20</property>
+                <property name="margin-left">30</property>
+                <property name="margin-right">30</property>
+
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">True</property>
+                    <property name="wrap">True</property>
+                    <property name="max-width-chars">60</property>
+                    <property name="xalign">0</property>
+                    <property name="halign">start</property>
+                    <property name="label" translatable="yes">A new virtual machine will be created and an 
operating system installed into it. Select an operating system source to begin.</property>
+                  </object>
+                </child>
+
+                <child>
+                  <object class="GtkBox" id="detected_sources_section">
+                    <property name="visible">True</property>
+                    <property name="spacing">10</property>
+                    <property name="orientation">vertical</property>
+
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="xalign">0</property>
+                        <property name="label" translatable="yes">Detected Sources</property>
+                        <style>
+                          <class name="bold-label"/>
+                        </style>
+                      </object>
+                    </child>
+
+                    <child>
+                      <object class="GtkBox">
+                        <property name="visible">True</property>
+                        <property name="orientation">vertical</property>
+                        <style>
+                          <class name="linked"/>
+                          <class name="frame"/>
+                          <class name="sources-list"/>
+                        </style>
+
+                        <child>
+                          <object class="GtkListBox" id="source_medias">
+                            <property name="visible">True</property>
+                            <signal name="row-activated" handler="on_source_media_selected"/>
+                            <style>
+                              <class name="sources-list"/>
+                            </style>
+                          </object>
+                        </child>
+
+                        <child>
+                          <object class="GtkButton" id="expand_detected_sources_list_button">
+                            <property name="visible">True</property>
+                            <signal name="clicked" handler="on_expand_detected_sources_list"/>
+                            <style>
+                              <class name="flat"/>
+                              <class name="list-expand-button"/>
+                              <class name="boxes-menu-row"/>
+                            </style>
+
+                            <child>
+                              <object class="GtkImage">
+                                <property name="visible">True</property>
+                                <property name="icon-name">view-more-symbolic</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="spacing">10</property>
+                    <property name="orientation">vertical</property>
+
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="xalign">0</property>
+                        <property name="label" translatable="yes">Featured Downloads</property>
+                        <style>
+                          <class name="bold-label"/>
+                        </style>
+                      </object>
+                    </child>
+
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="xalign">0</property>
+                        <property name="label" translatable="yes">You will be notified when the download has 
completed.</property>
+                      </object>
+                    </child>
+
+                    <child>
+                      <object class="GtkListBox" id="featured_medias">
+                        <property name="visible">True</property>
+                        <signal name="row-activated" handler="on_featured_media_selected"/>
+                        <style>
+                          <class name="frame"/>
+                          <class name="sources-list"/>
+                        </style>
+                      </object>
+                    </child>
+                  </object>
+                  </child>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="spacing">10</property>
+                    <property name="orientation">vertical</property>
+
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="xalign">0</property>
+                        <property name="label" translatable="yes">Select an OS Source</property>
+                        <style>
+                          <class name="bold-label"/>
+                        </style>
+                      </object>
+                    </child>
+
+                    <child>
+                      <object class="GtkBox">
+                        <property name="visible">True</property>
+                        <property name="orientation">vertical</property>
+                        <style>
+                          <class name="linked"/>
+                        </style>
+
+                        <child>
+                          <object class="GtkButton">
+                            <property name="visible">True</property>
+                            <signal name="clicked" handler="on_download_an_os_button_clicked"/>
+                            <style>
+                              <class name="boxes-menu-row"/>
+                            </style>
+
+                            <child>
+                              <object class="GtkGrid">
+                                <property name="visible">True</property>
+                                <property name="border-width">10</property>
+
+                                <child>
+                                  <object class="GtkLabel">
+                                    <property name="visible">True</property>
+                                    <property name="hexpand">True</property>
+                                    <property name="halign">start</property>
+                                    <property name="label" translatable="yes">Operating System 
Download</property>
+                                    <style>
+                                      <class name="bold-label"/>
+                                    </style>
+                                  </object>
+                                  <packing>
+                                    <property name="left-attach">0</property>
+                                    <property name="top-attach">0</property>
+                                  </packing>
+                                </child>
+
+                                <child>
+                                  <object class="GtkLabel">
+                                    <property name="visible">True</property>
+                                    <property name="hexpand">True</property>
+                                    <property name="halign">start</property>
+                                    <property name="label" translatable="yes">Browse and search for 
operating systems to install.</property>
+                                    <style>
+                                      <class name="dim-label"/>
+                                    </style>
+                                  </object>
+                                  <packing>
+                                    <property name="left-attach">0</property>
+                                    <property name="top-attach">1</property>
+                                  </packing>
+                                </child>
+
+                                <child>
+                                  <object class="GtkImage">
+                                    <property name="visible">True</property>
+                                    <property name="icon-name">go-next-symbolic</property>
+                                  </object>
+                                  <packing>
+                                    <property name="left-attach">1</property>
+                                    <property name="top-attach">0</property>
+                                    <property name="height">2</property>
+                                  </packing>
+                                </child>
+                              </object>
+                            </child>
+
+                          </object>
+                        </child>
+
+                        <child>
+                          <object class="GtkButton">
+                            <property name="visible">True</property>
+                            <signal name="clicked" handler="on_select_file_button_clicked"/>
+                            <style>
+                              <class name="boxes-menu-row"/>
+                            </style>
+
+                            <child>
+                              <object class="GtkGrid">
+                                <property name="visible">True</property>
+                                <property name="border-width">10</property>
+
+                                <child>
+                                  <object class="GtkLabel">
+                                    <property name="visible">True</property>
+                                    <property name="hexpand">True</property>
+                                    <property name="halign">start</property>
+                                    <property name="label" translatable="yes">Operating System Image 
File</property>
+                                    <style>
+                                      <class name="bold-label"/>
+                                    </style>
+                                  </object>
+                                  <packing>
+                                    <property name="left-attach">0</property>
+                                    <property name="top-attach">0</property>
+                                  </packing>
+                                </child>
+
+                                <child>
+                                  <object class="GtkLabel">
+                                    <property name="visible">True</property>
+                                    <property name="hexpand">True</property>
+                                    <property name="halign">start</property>
+                                    <property name="label" translatable="yes">Select an .iso file to install 
a virtual machine.</property>
+                                    <style>
+                                      <class name="dim-label"/>
+                                    </style>
+                                  </object>
+                                  <packing>
+                                    <property name="left-attach">0</property>
+                                    <property name="top-attach">1</property>
+                                  </packing>
+                                </child>
+
+                                <child>
+                                  <object class="GtkImage">
+                                    <property name="visible">True</property>
+                                    <property name="icon-name">go-next-symbolic</property>
+                                  </object>
+                                  <packing>
+                                    <property name="left-attach">1</property>
+                                    <property name="top-attach">0</property>
+                                    <property name="height">2</property>
+                                  </packing>
+                                </child>
+                              </object>
+                            </child>
+
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+
+        <child>
+          <object class="BoxesAssistantDownloadsPage" id="recommended_downloads_page">
+            <property name="visible">True</property>
+            <signal name="media-selected" handler="on_featured_media_selected"/>
+          </object>
+        </child>
+
+      </object>
+    </child>
+
+  </template>
+</interface>
diff --git a/data/ui/assistant/pages/preparation-page.ui b/data/ui/assistant/pages/preparation-page.ui
new file mode 100644
index 00000000..9ddec630
--- /dev/null
+++ b/data/ui/assistant/pages/preparation-page.ui
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="BoxesAssistantPreparationPage" parent="BoxesAssistantPage">
+    <property name="visible">True</property>
+    <property name="title" translatable="yes">Preparing…</property>
+
+    <child>
+      <object class="GtkGrid">
+        <property name="visible">True</property>
+        <property name="expand">True</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <property name="column-spacing">10</property>
+
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">True</property>
+            <property name="wrap">True</property>
+            <property name="halign">start</property>
+            <property name="label" translatable="yes">Preparing to create a new box</property>
+            <property name="margin-bottom">20</property>
+          </object>
+          <packing>
+            <property name="left-attach">0</property>
+            <property name="top-attach">0</property>
+            <property name="width">2</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkImage" id="installer_image">
+            <property name="visible">True</property>
+            <property name="icon-size">0</property>
+            <property name="pixel-size">128</property>
+            <property name="icon-name">media-optical</property>
+          </object>
+          <packing>
+            <property name="left-attach">0</property>
+            <property name="top-attach">1</property>
+            <property name="height">3</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkLabel" id="media_label">
+            <property name="visible">True</property>
+            <property name="halign">start</property>
+            <property name="valign">end</property>
+            <style>
+              <class name="boxes-wizard-media-os-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left-attach">1</property>
+            <property name="top-attach">1</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkLabel" id="status_label">
+            <property name="visible">True</property>
+            <property name="halign">start</property>
+            <property name="valign">center</property>
+          </object>
+          <packing>
+            <property name="left-attach">1</property>
+            <property name="top-attach">2</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkProgressBar" id="progress_bar">
+            <property name="visible">True</property>
+            <property name="valign">start</property>
+          </object>
+          <packing>
+            <property name="left-attach">1</property>
+            <property name="top-attach">3</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/ui/assistant/pages/review-page.ui b/data/ui/assistant/pages/review-page.ui
new file mode 100644
index 00000000..a135151d
--- /dev/null
+++ b/data/ui/assistant/pages/review-page.ui
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="BoxesAssistantReviewPage" parent="BoxesAssistantPage">
+    <property name="visible">True</property>
+    <property name="title" translatable="yes">Review and Create</property>
+    <property name="vexpand">False</property>
+    <property name="valign">start</property>
+    <property name="orientation">vertical</property>
+    <property name="border-width">30</property>
+    <property name="spacing">20</property>
+
+    <child>
+      <object class="GtkLabel" id="review_label">
+        <property name="visible">True</property>
+        <property name="halign">start</property>
+        <property name="wrap">True</property>
+        <property name="width-chars">30</property>
+        <property name="label" translatable="yes">Boxes is ready to set up a new box with the following 
properties:</property>
+      </object>
+    </child>
+
+    <child>
+      <object class="GtkInfoBar" id="nokvm_infobar">
+        <property name="visible">True</property>
+        <property name="halign">fill</property>
+        <property name="spacing">0</property>
+        <property name="message-type">warning</property>
+
+        <child internal-child="content_area">
+          <object class="GtkContainer" id="nokvm_container">
+            <property name="visible">True</property>
+
+            <child>
+              <object class="GtkImage" id="nokvm_image">
+                <property name="visible">True</property>
+                <property name="icon-name">dialog-warning</property>
+                <property name="icon-size">3</property>
+                <property name="pixel-size">48</property>
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkLabel" id="nokvm_label">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">Virtualization extensions are unavailable on your 
system.
+Check your BIOS settings to enable them.</property>
+                <property name="wrap">True</property>
+                <property name="halign">start</property>
+                <property name="hexpand">True</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">False</property>
+      </packing>
+    </child>
+
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">Resource Allocation</property>
+            <style>
+              <class name="bold-label"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkToggleButton" id="customize_button">
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">Customize</property>
+            <property name="halign">end</property>
+            <property name="hexpand">True</property>
+            <signal name="toggled" handler="on_customize_button_toggled"/>
+          </object>
+        </child>
+      </object>
+    </child>
+
+    <child>
+      <object class="GtkStack" id="customization_stack">
+        <property name="visible">True</property>
+        <child>
+          <object class="BoxesWizardSummary" id="summary"/>
+        </child>
+        <child>
+          <object class="GtkGrid" id="customization_grid">
+            <property name="visible">True</property>
+            <property name="row_spacing">10</property>
+            <property name="column_spacing">20</property>
+          </object>
+        </child>
+      </object>
+
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">False</property>
+      </packing>
+    </child>
+
+  </template>
+</interface>
diff --git a/data/ui/assistant/pages/setup-page.ui b/data/ui/assistant/pages/setup-page.ui
new file mode 100644
index 00000000..f5425b7a
--- /dev/null
+++ b/data/ui/assistant/pages/setup-page.ui
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="BoxesAssistantSetupPage" parent="BoxesAssistantPage">
+    <property name="visible">True</property>
+    <property name="title" translatable="yes">Express Installation</property>
+    <property name="expand">True</property>
+
+    <child>
+      <object class="GtkBox" id="setup_box">
+        <property name="visible">True</property>
+        <property name="expand">True</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">0</property>
+        <property name="margin-start">10</property>
+        <property name="margin-end">10</property>
+        <property name="valign">center</property>
+      </object>
+    </child>
+
+  </template>
+</interface>
diff --git a/data/ui/assistant/rhel-download-dialog.ui b/data/ui/assistant/rhel-download-dialog.ui
new file mode 100644
index 00000000..ad8b9e1c
--- /dev/null
+++ b/data/ui/assistant/rhel-download-dialog.ui
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="BoxesRHELDownloadDialog" parent="GtkDialog">
+    <property name="modal">True</property>
+    <property name="type-hint">dialog</property>
+    <property name="height-request">250</property>
+
+    <child internal-child="vbox">
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+
+        <child>
+          <object class="GtkOverlay" id="overlay">
+            <property name="visible">True</property>
+
+            <child type="overlay">
+              <object class="GtkProgressBar" id="progress_bar">
+                <property name="no-show-all">True</property>
+                <property name="valign">start</property>
+                <style>
+                  <class name="osd"/>
+                </style>
+              </object>
+            </child>
+
+            <child>
+              <!-- https://bugzilla.gnome.org/show_bug.cgi?id=786932 -->
+              <!-- https://bugzilla.gnome.org/show_bug.cgi?id=787033 -->
+              <!-- https://bugs.webkit.org/show_bug.cgi?id=175937 -->
+              <object class="WebKitWebView" type-func="webkit_web_view_get_type" id="web_view">
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <property name="visible">True</property>
+                <signal name="context-menu" handler="on_context_menu" />
+                <signal name="notify::estimated-load-progress" handler="on_notify_estimated_load_progress" />
+                <signal name="decide-policy" handler="on_decide_policy"/>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/ui/assistant/vm-assistant.ui b/data/ui/assistant/vm-assistant.ui
new file mode 100644
index 00000000..71744f21
--- /dev/null
+++ b/data/ui/assistant/vm-assistant.ui
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="BoxesVMAssistant" parent="GtkDialog">
+    <property name="modal">True</property>
+    <property name="type-hint">dialog</property>
+    <property name="title" translatable="yes">Create a Virtual Machine</property>
+    <property name="width-request">724</property>
+    <property name="height-request">568</property>
+
+    <child internal-child="vbox">
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+
+        <child>
+          <object class="GtkStack" id="pages">
+            <property name="visible">True</property>
+            <signal name="notify::visible-child" handler="update_titlebar"/>
+            <child>
+              <object class="BoxesAssistantIndexPage" id="index_page">
+                <signal name="done" handler="do_preparation"/>
+              </object>
+            </child>
+            <child>
+              <object class="BoxesAssistantPreparationPage" id="preparation_page">
+                <signal name="done" handler="do_setup"/>
+              </object>
+            </child>
+            <child>
+              <object class="BoxesAssistantSetupPage" id="setup_page">
+                <signal name="done" handler="do_review"/>
+              </object>
+            </child>
+            <child>
+              <object class="BoxesAssistantReviewPage" id="review_page">
+                <signal name="done" handler="do_create"/>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+
+    <child type="action">
+      <object class="GtkButton" id="previous_button">
+        <property name="visible">True</property>
+        <property name="label" translatable="yes">Cancel</property>
+        <signal name="clicked" handler="on_previous_button_clicked"/>
+      </object>
+    </child>
+    <child type="action">
+      <object class="GtkButton" id="next_button">
+        <property name="visible">False</property>
+        <property name="label" translatable="yes">Next</property>
+        <signal name="clicked" handler="on_next_button_clicked"/>
+      </object>
+    </child>
+
+    <action-widgets>
+      <action-widget response="cancel">previous_button</action-widget>
+      <action-widget response="ok">next_button</action-widget>
+    </action-widgets>
+
+  </template>
+</interface>
diff --git a/data/ui/collection-toolbar.ui b/data/ui/collection-toolbar.ui
index a9810b6e..5a13191d 100644
--- a/data/ui/collection-toolbar.ui
+++ b/data/ui/collection-toolbar.ui
@@ -61,6 +61,33 @@
       </object>
     </child>
 
+    <child>
+      <object class="GtkMenuButton" id="downloads_hub_btn">
+        <property name="visible">False</property>
+        <property name="valign">center</property>
+        <property name="use-underline">True</property>
+        <style>
+          <class name="image-button"/>
+        </style>
+        <child internal-child="accessible">
+          <object class="AtkObject">
+            <property name="accessible-name" translatable="yes">Downloads</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkImage">
+            <property name="visible">True</property>
+            <property name="icon-name">media-record-symbolic</property>
+            <property name="icon-size">1</property>
+          </object>
+        </child>
+      </object>
+
+      <packing>
+        <property name="pack-type">end</property>
+      </packing>
+    </child>
+
     <child>
       <object class="GtkMenuButton" id="hamburger_btn">
         <property name="visible">True</property>
@@ -210,7 +237,7 @@
           <object class="GtkModelButton">
             <property name="visible">True</property>
             <property name="text" translatable="yes">Create a Virtual Machine…</property>
-            <signal name="clicked" handler="on_new_vm_btn_clicked"/>
+            <signal name="clicked" handler="on_create_vm_btn_clicked"/>
           </object>
         </child>
         <child>
diff --git a/data/ui/downloads-hub-row.ui b/data/ui/downloads-hub-row.ui
new file mode 100644
index 00000000..daf6a026
--- /dev/null
+++ b/data/ui/downloads-hub-row.ui
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.19"/>
+  <template class="BoxesDownloadsHubRow" parent="GtkListBoxRow">
+    <property name="visible">True</property>
+    <property name="selectable">False</property>
+
+    <child>
+      <object class="GtkGrid">
+        <property name="visible">True</property>
+        <property name="border-width">10</property>
+        <property name="column-spacing">20</property>
+
+        <child>
+          <object class="GtkImage" id="image">
+            <property name="visible">True</property>
+            <property name="icon-name">media-optical</property>
+            <property name="icon-size">0</property>
+            <property name="pixel-size">64</property>
+          </object>
+          <packing>
+            <property name="height">2</property>
+            <property name="left-attach">0</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkLabel" id="label">
+            <property name="visible">True</property>
+            <property name="wrap">True</property>
+            <property name="max-width-chars">40</property>
+            <property name="halign">start</property>
+          </object>
+          <packing>
+            <property name="left-attach">1</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkProgressBar" id="progress_bar">
+            <property name="visible">True</property>
+            <property name="hexpand">True</property>
+          </object>
+          <packing>
+            <property name="left-attach">1</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkButton">
+            <property name="visible">True</property>
+            <property name="valign">center</property>
+            <signal name="clicked" handler="cancel_download"/>
+
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="icon-name">window-close-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="left-attach">2</property>
+            <property name="height">2</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/ui/downloads-hub.ui b/data/ui/downloads-hub.ui
new file mode 100644
index 00000000..5d0665c2
--- /dev/null
+++ b/data/ui/downloads-hub.ui
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.19"/>
+  <template class="BoxesDownloadsHub" parent="GtkPopover">
+    <property name="can_focus">False</property>
+    <property name="modal">True</property>
+    <property name="position">bottom</property>
+
+    <child>
+      <object class="GtkListBox" id="listbox">
+        <property name="visible">True</property>
+        <signal name="row-activated" handler="on_row_activated"/>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/ui/wizard-downloadable-entry.ui b/data/ui/wizard-downloadable-entry.ui
index 10e4bb9c..480244e3 100644
--- a/data/ui/wizard-downloadable-entry.ui
+++ b/data/ui/wizard-downloadable-entry.ui
@@ -3,9 +3,6 @@
   <!-- interface-requires gtk+ 3.9 -->
   <template class="BoxesWizardDownloadableEntry" parent="GtkListBoxRow">
     <property name="visible">True</property>
-    <style>
-      <class name="boxes-menu-row"/>
-    </style>
 
     <child>
       <object class="GtkBox">
diff --git a/data/ui/wizard-media-entry.ui b/data/ui/wizard-media-entry.ui
index 401470cd..050e3415 100644
--- a/data/ui/wizard-media-entry.ui
+++ b/data/ui/wizard-media-entry.ui
@@ -2,10 +2,11 @@
 <interface>
   <!-- interface-requires gtk+ 3.9 -->
   <template class="BoxesWizardMediaEntry" parent="GtkListBoxRow">
-    <property name="visible">True</property>
     <style>
-      <class name="boxes-menu-row"/>
+      <class name="entry-row"/>
     </style>
+
+    <property name="visible">True</property>
     <child>
       <object class="GtkBox" id="hbox">
         <property name="visible">True</property>
diff --git a/src/app-window.vala b/src/app-window.vala
index 82a9f834..46d20ed4 100644
--- a/src/app-window.vala
+++ b/src/app-window.vala
@@ -232,7 +232,7 @@ private void ui_state_changed () {
                                             icon_view,
                                             list_view,
                                             props_window,
-                                            wizard_window,
+                                            //wizard_window,
                                             empty_boxes }) {
             ui.set_state (ui_state);
         }
@@ -303,6 +303,10 @@ public void show_remote_connection_assistant () {
         new Boxes.RemoteConnectionAssistant (this).run ();
     }
 
+    public void show_vm_assistant (string? path = null) {
+        new Boxes.VMAssistant (this, path).run ();
+    }
+
     public void show_properties () {
         if (current_item != null) {
             if (ui_state == UIState.COLLECTION && selection_mode)
diff --git a/src/app.vala b/src/app.vala
index c77b5e04..dd6509d8 100644
--- a/src/app.vala
+++ b/src/app.vala
@@ -91,6 +91,10 @@ public App () {
         action.activate.connect ((param) => { open_name (param.get_string ()); });
         add_action (action);
 
+        action = new GLib.SimpleAction ("install", GLib.VariantType.STRING);
+        action.activate.connect ((param) => { install (param.get_string ()); });
+        add_action (action);
+
         action = new GLib.SimpleAction ("about", null);
         action.activate.connect (() => {
             string[] authors = {
@@ -313,6 +317,10 @@ public void open_name (string name) {
         }
     }
 
+    public void install (string path) {
+        main_window.show_vm_assistant (path);
+    }
+
     public bool open_uuid (string uuid) {
         main_window.set_state (UIState.COLLECTION);
 
@@ -450,7 +458,7 @@ private async void setup_default_source () ensures (default_connection != null)
         }
     }
 
-    private new void send_notification (string notification_id, GLib.Notification notification) {
+    public new void send_notification (string notification_id, GLib.Notification notification) {
         base.send_notification (notification_id, notification);
 
         system_notifications.append (notification_id);
diff --git a/src/assistant/assistant-page.vala b/src/assistant/assistant-page.vala
new file mode 100644
index 00000000..589500db
--- /dev/null
+++ b/src/assistant/assistant-page.vala
@@ -0,0 +1,17 @@
+using Gtk;
+
+private abstract class Boxes.AssistantPage : Gtk.Box {
+    protected Object? artifact;
+    public bool skip = false;
+    protected signal void done (Object artifact);
+
+    public virtual string title {
+        protected set; get;
+    }
+
+    public async virtual void next () {
+        done (artifact);
+    }
+
+    public abstract void cleanup ();
+}
diff --git a/src/assistant/downloads-page.vala b/src/assistant/downloads-page.vala
new file mode 100644
index 00000000..41b93581
--- /dev/null
+++ b/src/assistant/downloads-page.vala
@@ -0,0 +1,118 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+
+public enum AssistantDownloadsPageView {
+    RECOMMENDED,
+    SEARCH_RESULTS,
+    NO_RESULTS,
+}
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/pages/downloads-page.ui")]
+public class Boxes.AssistantDownloadsPage : Gtk.Stack {
+    private OSDatabase os_db = new OSDatabase ();
+    public DownloadsSearch search { private set; get; }
+
+    [GtkChild]
+    private Gtk.ListBox listbox;
+    [GtkChild]
+    private Gtk.ListBox recommended_listbox;
+
+    public Gtk.SearchEntry search_entry = new Gtk.SearchEntry ();
+    private GLib.ListStore recommended_model;
+
+    public signal void media_selected (Gtk.ListBoxRow row);
+
+    private AssistantDownloadsPageView _page;
+    public AssistantDownloadsPageView page {
+        get { return _page; }
+        set {
+            _page = value;
+
+            switch (_page) {
+                case AssistantDownloadsPageView.SEARCH_RESULTS:
+                    visible_child_name = "search-results";
+                    break;
+                case AssistantDownloadsPageView.NO_RESULTS:
+                    visible_child_name = "no-results";
+                    break;
+                case AssistantDownloadsPageView.RECOMMENDED:
+                default:
+                    visible_child_name = "recommended";
+                    break;
+            }
+        }
+    }
+
+    construct {
+        os_db.load.begin ();
+
+        search = new DownloadsSearch ();
+
+        // TODO: move this into a UI file
+        search_entry.search_changed.connect (on_search_changed);
+        search_entry.width_chars = 50;
+        search_entry.can_focus = true;
+        search_entry.placeholder_text = _("Search for an OS or enter a download link…");
+        search_entry.visible = true;
+
+        recommended_model = new GLib.ListStore (typeof (Osinfo.Media));
+        recommended_listbox.bind_model (recommended_model, create_downloads_entry);
+        recommended_listbox.set_header_func (use_list_box_separator);
+        populate_recommended_list.begin ();
+
+        listbox.bind_model (search.model, create_downloads_entry);
+        listbox.set_header_func (use_list_box_separator);
+
+        search.search_changed.connect (set_visible_view);
+    }
+
+    private void set_visible_view () {
+        if (search.text.length == 0) {
+            page = AssistantDownloadsPageView.RECOMMENDED;
+        } else if (search.model.get_n_items () == 0) {
+            page = AssistantDownloadsPageView.NO_RESULTS;
+        } else {
+            page = AssistantDownloadsPageView.SEARCH_RESULTS;
+        }
+    }
+
+    private async void populate_recommended_list () {
+        foreach (var media in yield get_recommended_downloads ()) {
+            if (media != null) {
+                recommended_model.append (media);
+            }
+        }
+    }
+
+    private Gtk.Widget create_downloads_entry (Object item) {
+        return new WizardDownloadableEntry (item as Osinfo.Media);
+    }
+
+    [GtkCallback]
+    private void on_listbox_row_activated (Gtk.ListBoxRow row) {
+        media_selected (row);
+    }
+
+    [GtkCallback]
+    private void on_show_more_button_clicked () {
+        search.show_all ();
+
+        page = AssistantDownloadsPageView.SEARCH_RESULTS;
+    }
+
+    private void on_search_changed () {
+        var text = search_entry.get_text ();
+
+        if (text == null)
+            return;
+
+        search.text = text;
+    }
+
+    [GtkCallback]
+    private bool on_key_pressed (Gtk.Widget widget, Gdk.EventKey event) {
+        if (!search_entry.has_focus)
+            search_entry.grab_focus ();
+
+        return search_entry.key_press_event (event);
+    }
+}
diff --git a/src/assistant/index-page.vala b/src/assistant/index-page.vala
new file mode 100644
index 00000000..8587dd48
--- /dev/null
+++ b/src/assistant/index-page.vala
@@ -0,0 +1,159 @@
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/pages/index-page.ui")]
+private class Boxes.AssistantIndexPage : AssistantPage {
+    GLib.ListStore source_model = new GLib.ListStore (typeof (InstallerMedia));
+    GLib.ListStore featured_model = new GLib.ListStore (typeof (Osinfo.Media));
+
+    private VMAssistant dialog;
+
+    private GLib.List<InstallerMedia> installer_medias;
+
+    private const int MAX_MEDIA_ENTRIES = 3;
+
+    [GtkChild]
+    private Stack stack;
+    [GtkChild]
+    private AssistantDownloadsPage recommended_downloads_page;
+    [GtkChild]
+    private ScrolledWindow home_page;
+
+    [GtkChild]
+    private Box detected_sources_section;
+    [GtkChild]
+    private ListBox source_medias;
+    [GtkChild]
+    private ListBox featured_medias;
+    [GtkChild]
+    private Button expand_detected_sources_list_button;
+
+    private GLib.Cancellable cancellable = new GLib.Cancellable ();
+
+    construct {
+        populate_media_lists.begin ();
+
+        source_medias.bind_model (source_model, add_media_entry);
+        featured_medias.bind_model (featured_model, add_featured_media_entry);
+
+        source_medias.set_header_func (use_list_box_separator);
+        featured_medias.set_header_func (use_list_box_separator);
+    }
+
+    public void setup (VMAssistant dialog) {
+        this.dialog = dialog;
+    }
+
+    public void go_back () {
+        if (stack.visible_child == home_page) {
+            dialog.shutdown ();
+
+            return;
+        }
+
+        stack.visible_child = home_page;
+        update_topbar ();
+    }
+
+    private async void populate_media_lists () {
+        var media_manager = MediaManager.get_instance ();
+
+        installer_medias = yield media_manager.list_installer_medias ();
+        populate_detected_sources_list (MAX_MEDIA_ENTRIES);
+
+        var recommended_downloads = yield get_recommended_downloads ();
+        for (var i = 0; i < MAX_MEDIA_ENTRIES; i++)
+            featured_model.append (recommended_downloads.nth (i).data);
+    }
+
+    private void populate_detected_sources_list (int? number_of_items = null) {
+       var number_of_available_medias = installer_medias.length ();
+        detected_sources_section.visible = (number_of_available_medias > 0);
+        source_model.remove_all ();
+
+        if (number_of_available_medias == 0)
+            return;
+
+       expand_detected_sources_list_button.visible = (number_of_available_medias > MAX_MEDIA_ENTRIES);
+
+        foreach (var media in installer_medias) {
+            source_model.append (media);
+
+            if (number_of_items != null && ((number_of_items -= 1) == 0))
+                return;
+        }
+    }
+
+    private Gtk.Widget add_media_entry (GLib.Object object) {
+        return new WizardMediaEntry (object as InstallerMedia);
+    }
+
+    private Gtk.Widget add_featured_media_entry (GLib.Object object) {
+        return new WizardDownloadableEntry (object as Osinfo.Media);
+    }
+
+    [GtkCallback]
+    private void update_topbar () {
+        dialog.previous_button.label = _("Cancel");
+
+        var titlebar = dialog.get_titlebar () as Gtk.HeaderBar;
+        if (stack.visible_child == recommended_downloads_page) {
+            titlebar.set_custom_title (recommended_downloads_page.search_entry);
+        } else {
+            titlebar.set_custom_title (null);
+        }
+    }
+
+    [GtkCallback]
+    private void on_expand_detected_sources_list () {
+        populate_detected_sources_list ();
+
+        expand_detected_sources_list_button.hide ();
+    }
+
+    [GtkCallback]
+    private void on_source_media_selected (Gtk.ListBoxRow row) {
+        var entry = row as WizardMediaEntry;
+
+        if (entry.media != null)
+            done (entry.media);
+    }
+
+    [GtkCallback]
+    private void on_featured_media_selected (Gtk.ListBoxRow row) {
+        var entry = row as WizardDownloadableEntry;
+
+        if (entry.os != null && entry.os.id.has_prefix ("http://redhat.com/rhel/";)) {
+            (new RHELDownloadDialog (dialog, entry).run ());
+        } else {
+            DownloadsHub.get_instance ().add_item (entry);
+        }
+
+        dialog.shutdown ();
+    }
+
+    public override void cleanup () {
+        cancellable.cancel ();
+    }
+
+    [GtkCallback]
+    private async void on_select_file_button_clicked () {
+        var file_chooser = new Gtk.FileChooserNative (_("Select a device or ISO file"),
+                                                      App.app.main_window,
+                                                      Gtk.FileChooserAction.OPEN,
+                                                      _("Open"), _("Cancel"));
+        file_chooser.bind_property ("visible", dialog, "visible", BindingFlags.INVERT_BOOLEAN);
+        if (file_chooser.run () == Gtk.ResponseType.ACCEPT) {
+            var media_manager = MediaManager.get_instance ();
+            var media = yield media_manager.create_installer_media_for_path (file_chooser.get_filename (),
+                                                                             cancellable);
+            done (media);
+        }
+    }
+
+    [GtkCallback]
+    private void on_download_an_os_button_clicked () {
+        stack.set_visible_child (recommended_downloads_page);
+
+        dialog.previous_button.label = _("Previous");
+    }
+}
diff --git a/src/assistant/preparation-page.vala b/src/assistant/preparation-page.vala
new file mode 100644
index 00000000..aa6d426d
--- /dev/null
+++ b/src/assistant/preparation-page.vala
@@ -0,0 +1,69 @@
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/pages/preparation-page.ui")]
+private class Boxes.AssistantPreparationPage : AssistantPage {
+    [GtkChild]
+    private Gtk.Label media_label;
+    [GtkChild]
+    private Gtk.Label status_label;
+    [GtkChild]
+    private Gtk.Image installer_image;
+    [GtkChild]
+    private Gtk.ProgressBar progress_bar;
+
+    private Cancellable cancellable = new GLib.Cancellable ();
+
+    private InstallerMedia _media;
+    public InstallerMedia media {
+        get { return _media; }
+        set {
+            _media = value;
+
+            if (_media.os != null) {
+                media_label.label = _media.os.name;
+                Downloader.fetch_os_logo.begin (installer_image, _media.os, 128);
+            }
+        }
+    }
+
+    public void setup (InstallerMedia media) {
+        try {
+            var media_manager = MediaManager.get_instance ();
+            media = media_manager.create_installer_media_from_media (media);
+        } catch (GLib.Error error) {
+            warning ("Failed to setup installation media '%s': %s", media.device_file, error.message);
+        }
+
+        prepare (media);
+
+        skip = true;
+    }
+
+    public async void prepare (InstallerMedia media) {
+        var progress = create_preparation_progress ();
+        if (!yield media.prepare (progress, cancellable)) // add cancellable
+            return;
+
+        progress_bar.fraction = 1.0;
+
+        done (media.get_vm_creator ());
+    }
+
+    private ActivityProgress create_preparation_progress () {
+        var progress = new ActivityProgress ();
+
+        progress.notify["progress"].connect (() => {
+            if (progress.progress - progress_bar.fraction >= 0.01)
+                progress_bar.fraction = progress.progress;
+        });
+        progress_bar.fraction = progress.progress = 0;
+
+        progress.bind_property ("info", status_label, "label");
+
+        return progress;
+    }
+
+    public override void cleanup () {
+        cancellable.reset ();
+    }
+}
diff --git a/src/assistant/review-page.vala b/src/assistant/review-page.vala
new file mode 100644
index 00000000..afda063e
--- /dev/null
+++ b/src/assistant/review-page.vala
@@ -0,0 +1,123 @@
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/pages/review-page.ui")]
+private class Boxes.AssistantReviewPage : AssistantPage {
+    [GtkChild]
+    private WizardSummary summary;
+    [GtkChild]
+    private InfoBar nokvm_infobar;
+    [GtkChild]
+    private Grid customization_grid;
+    [GtkChild]
+    private ToggleButton customize_button;
+    [GtkChild]
+    private Stack customization_stack;
+
+    private Cancellable cancellable = new GLib.Cancellable ();
+
+    [GtkCallback]
+    private void on_customize_button_toggled () {
+        customization_stack.set_visible_child (customize_button.active ?
+                                               customization_grid : summary);
+    }
+
+    public async void setup (VMCreator vm_creator) {
+        try {
+            artifact = yield vm_creator.create_vm (cancellable);
+        } catch (IOError.CANCELLED cancel_error) { // We did this, so ignore!
+        } catch (GLib.Error error) {
+            warning ("Box setup failed: %s", error.message);
+        }
+
+        yield populate (artifact as LibvirtMachine);
+    }
+
+    public async void populate (LibvirtMachine machine) {
+        var vm_creator = machine.vm_creator;
+        foreach (var property in vm_creator.install_media.get_vm_properties ())
+            summary.add_property (property.first, property.second);
+
+        try {
+            var config = null as GVirConfig.Domain;
+            yield App.app.async_launcher.launch (() => {
+                config = machine.domain.get_config (GVir.DomainXMLFlags.INACTIVE);
+            });
+
+            var memory = format_size (config.memory * Osinfo.KIBIBYTES, FormatSizeFlags.IEC_UNITS);
+            summary.add_property (_("Memory"), memory);
+        } catch (GLib.Error error) {
+            warning ("Failed to get configuration for machine '%s': %s", machine.name, error.message);
+        }
+
+        if (!machine.importing && machine.storage_volume != null) {
+            try {
+                var volume_info = machine.storage_volume.get_info ();
+                var capacity = format_size (volume_info.capacity);
+                summary.add_property (_("Disk"),
+                                      // Translators: This is disk size. E.g "1 GB maximum".
+                                      _("%s maximum").printf (capacity));
+            } catch (GLib.Error error) {
+                warning ("Failed to get information on volume '%s': %s",
+                         machine.storage_volume.get_name (),
+                         error.message);
+            }
+
+            nokvm_infobar.visible = (machine.domain_config.get_virt_type () != 
GVirConfig.DomainVirtType.KVM);
+        }
+
+        populate_customization_grid (machine);
+    }
+
+    private void populate_customization_grid (LibvirtMachine machine) {
+        var resource_properties = new GLib.List<Boxes.Property> ();
+        machine.properties.get_resources_properties (ref resource_properties);
+
+        return_if_fail (resource_properties.length () > 0);
+
+        var current_row = 0;
+        foreach (var property in resource_properties) {
+            if (property.widget == null || property.extra_widget == null) {
+                warn_if_reached ();
+
+                continue;
+            }
+
+            property.widget.hexpand = true;
+            customization_grid.attach (property.widget, 0, current_row, 1, 1);
+
+            property.extra_widget.hexpand = true;
+            customization_grid.attach (property.extra_widget, 0, current_row + 1, 1, 1);
+
+            current_row += 2;
+        }
+        customization_grid.show_all ();
+    }
+
+    public override void cleanup () {
+        cancellable.cancel ();
+
+        summary.clear ();
+        nokvm_infobar.hide ();
+
+        if (artifact != null) {
+            App.app.delete_machine (artifact as Machine);
+        }
+
+        foreach (var child in customization_grid.get_children ())
+            customization_grid.remove (child);
+    }
+
+    public override async void next () {
+        if (artifact == null) {
+            var wait = notify["artifact"].connect (() => {
+                next.callback ();
+            });
+            yield;
+            disconnect (wait);
+        }
+
+        done (artifact);
+
+        cancellable.reset ();
+    }
+}
diff --git a/src/assistant/rhel-download-dialog.vala b/src/assistant/rhel-download-dialog.vala
new file mode 100644
index 00000000..ae41017a
--- /dev/null
+++ b/src/assistant/rhel-download-dialog.vala
@@ -0,0 +1,182 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/rhel-download-dialog.ui")]
+private class Boxes.RHELDownloadDialog : Gtk.Dialog {
+    [GtkChild]
+    private Gtk.ProgressBar progress_bar;
+    [GtkChild]
+    private WebKit.WebView web_view;
+
+    private uint hide_progress_bar_id;
+    private const uint progress_bar_id_timeout = 500;  // 500ms
+
+    private bool is_rhel8 = false;
+
+    private GLib.Cancellable cancellable = new GLib.Cancellable ();
+
+    private WizardDownloadableEntry entry;
+
+    construct {
+        var context = web_view.get_context ();
+        var language_names = GLib.Intl.get_language_names ();
+        context.set_preferred_languages (language_names);
+
+        cancellable.connect (() => {
+            web_view.stop_loading ();
+            web_view.load_uri ("about:blank");
+
+            var data_manager = web_view.get_website_data_manager ();
+            data_manager.clear.begin (WebKit.WebsiteDataTypes.COOKIES, 0, null);
+        });
+    }
+
+    public RHELDownloadDialog (VMAssistant dialog, WizardDownloadableEntry entry) {
+        set_transient_for (App.app.main_window);
+        this.entry = entry;
+
+        var user_agent = GLib.Uri.escape_string (get_user_agent (), null, false);
+        var authentication_uri = "https://developers.redhat.com/download-manager/rest/featured/file/rhel"; +
+                                 "?tag=" + user_agent;
+
+        var os = entry.os;
+        is_rhel8 = os.id.has_prefix ("http://redhat.com/rhel/8";);
+
+        web_view.load_uri (authentication_uri);
+
+        int width, height;
+        dialog.get_size_request (out width, out height);
+        set_size_request (width, height);
+
+        bind_property ("visible", dialog, "visible", BindingFlags.INVERT_BOOLEAN);
+    }
+
+    [GtkCallback]
+    private bool on_decide_policy (WebKit.WebView web_view,
+                                   WebKit.PolicyDecision decision,
+                                   WebKit.PolicyDecisionType decision_type) {
+        if (decision_type != WebKit.PolicyDecisionType.NAVIGATION_ACTION)
+            return false;
+
+        var action = (decision as WebKit.NavigationPolicyDecision).get_navigation_action ();
+        var request = action.get_request ();
+        var request_uri = request.get_uri ();
+
+        if (!request_uri.has_prefix ("https://developers.redhat.com/products/rhel";) &&
+            !request_uri.has_prefix ("https://access.cdn.redhat.com";))
+            return false;
+
+        var soup_request_uri = new Soup.URI (request_uri);
+        var query = soup_request_uri.get_query ();
+        if (query == null)
+            return false;
+
+        var key_value_pairs = Soup.Form.decode (query);
+
+        var download_uri = is_rhel8 ? request_uri : key_value_pairs.lookup ("tcDownloadURL");
+        if (download_uri == null)
+            return false;
+
+        debug ("RHEL ISO download URI: %s", download_uri);
+
+        entry.url = download_uri;
+        DownloadsHub.get_instance ().add_item (entry);
+
+        decision.ignore ();
+        this.close ();
+
+        return true;
+    }
+
+    public override void dispose () {
+        if (hide_progress_bar_id != 0) {
+            GLib.Source.remove (hide_progress_bar_id);
+            hide_progress_bar_id = 0;
+        }
+
+        base.dispose ();
+    }
+
+    [GtkCallback]
+    private bool on_context_menu (WebKit.WebView web_view,
+                                  WebKit.ContextMenu context_menu,
+                                  Gdk.Event event,
+                                  WebKit.HitTestResult hit_test_result) {
+        var items_to_remove = new GLib.List<WebKit.ContextMenuItem> ();
+
+        foreach (var item in context_menu.get_items ()) {
+            var action = item.get_stock_action ();
+            if (action == WebKit.ContextMenuAction.GO_BACK ||
+                action == WebKit.ContextMenuAction.GO_FORWARD ||
+                action == WebKit.ContextMenuAction.DOWNLOAD_AUDIO_TO_DISK ||
+                action == WebKit.ContextMenuAction.DOWNLOAD_IMAGE_TO_DISK ||
+                action == WebKit.ContextMenuAction.DOWNLOAD_LINK_TO_DISK ||
+                action == WebKit.ContextMenuAction.DOWNLOAD_VIDEO_TO_DISK ||
+                action == WebKit.ContextMenuAction.OPEN_AUDIO_IN_NEW_WINDOW ||
+                action == WebKit.ContextMenuAction.OPEN_FRAME_IN_NEW_WINDOW ||
+                action == WebKit.ContextMenuAction.OPEN_IMAGE_IN_NEW_WINDOW ||
+                action == WebKit.ContextMenuAction.OPEN_LINK_IN_NEW_WINDOW ||
+                action == WebKit.ContextMenuAction.OPEN_VIDEO_IN_NEW_WINDOW ||
+                action == WebKit.ContextMenuAction.RELOAD ||
+                action == WebKit.ContextMenuAction.STOP) {
+                items_to_remove.prepend (item);
+            }
+        }
+
+        foreach (var item in items_to_remove) {
+            context_menu.remove (item);
+        }
+
+        var separators_to_remove = new GLib.List<WebKit.ContextMenuItem> ();
+        WebKit.ContextMenuAction previous_action = WebKit.ContextMenuAction.NO_ACTION; // same as a separator
+
+        foreach (var item in context_menu.get_items ()) {
+            var action = item.get_stock_action ();
+            if (action == WebKit.ContextMenuAction.NO_ACTION && action == previous_action)
+                separators_to_remove.prepend (item);
+
+            previous_action = action;
+        }
+
+        foreach (var item in separators_to_remove) {
+            context_menu.remove (item);
+        }
+
+        var n_items = context_menu.get_n_items ();
+        return n_items == 0;
+    }
+
+    [GtkCallback]
+    private void on_notify_estimated_load_progress () {
+        if (hide_progress_bar_id != 0) {
+            GLib.Source.remove (hide_progress_bar_id);
+            hide_progress_bar_id = 0;
+        }
+
+        string? uri = web_view.get_uri ();
+        if (uri == null || uri == "about:blank")
+            return;
+
+        var progress = web_view.get_estimated_load_progress ();
+        bool loading = web_view.is_loading;
+
+        if (progress == 1.0 || !loading) {
+            hide_progress_bar_id = GLib.Timeout.add (progress_bar_id_timeout, () => {
+                progress_bar.hide ();
+                hide_progress_bar_id = 0;
+                return GLib.Source.REMOVE;
+            });
+        } else {
+            progress_bar.show ();
+        }
+
+        progress_bar.set_fraction (loading || progress == 1.0 ? progress : 0.0);
+    }
+
+    public override void close () {
+        cancellable.cancel ();
+
+        base.close ();
+        destroy ();
+    }
+}
diff --git a/src/assistant/setup-page.vala b/src/assistant/setup-page.vala
new file mode 100644
index 00000000..64279ebd
--- /dev/null
+++ b/src/assistant/setup-page.vala
@@ -0,0 +1,34 @@
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/pages/setup-page.ui")]
+private class Boxes.AssistantSetupPage : AssistantPage {
+    [GtkChild]
+    private Box setup_box;
+
+    public async void setup (VMCreator vm_creator) {
+        this.artifact = vm_creator;
+
+        vm_creator.install_media.populate_setup_box (setup_box);
+        if (!vm_creator.install_media.need_user_input_for_vm_creation &&
+             vm_creator.install_media.ready_to_create) {
+            done (vm_creator);
+        }
+
+        skip = !vm_creator.install_media.need_user_input_for_vm_creation;
+    }
+
+    public override async void next () {
+        var vm_creator = artifact as VMCreator;
+        if (vm_creator.install_media.ready_to_create) {
+            done (vm_creator);
+        }
+    }
+
+    public override void cleanup () {
+        if (!skip)
+            return;
+
+        foreach (var child in setup_box.get_children ())
+            child.destroy ();
+    }
+}
diff --git a/src/assistant/vm-assistant.vala b/src/assistant/vm-assistant.vala
new file mode 100644
index 00000000..f63faee9
--- /dev/null
+++ b/src/assistant/vm-assistant.vala
@@ -0,0 +1,149 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/vm-assistant.ui")]
+private class Boxes.VMAssistant : Gtk.Dialog {
+    [GtkChild]
+    private Stack pages;
+    [GtkChild]
+    private AssistantIndexPage index_page;
+    [GtkChild]
+    private AssistantPreparationPage preparation_page;
+    [GtkChild]
+    private AssistantSetupPage setup_page;
+    [GtkChild]
+    private AssistantReviewPage review_page;
+
+    [GtkChild]
+    public Button previous_button;
+    [GtkChild]
+    private Button next_button;
+
+    private AssistantPage visible_page {
+        get {
+            return pages.get_visible_child () as AssistantPage;
+        }
+    }
+
+    private AssistantPage? previous_page {
+        get {
+            var current_page_index = pages.get_children ().index (visible_page);
+            return pages.get_children ().nth_data (current_page_index - 1) as AssistantPage;
+        }
+    }
+
+    construct {
+        use_header_bar = 1;
+    }
+
+    public VMAssistant (AppWindow app_window, string? path = null) {
+        set_transient_for (app_window);
+
+        // TODO: Make the Assistant independent from window states
+        app_window.set_state (UIState.WIZARD);
+
+        index_page.setup (this);
+
+        if (path != null)
+            prepare_for_path.begin (path);
+    }
+
+    private async void prepare_for_path (string path) {
+        var media_manager = MediaManager.get_instance ();
+
+        try {
+            var installer_media = yield media_manager.create_installer_media_for_path (path, null);
+            do_preparation (installer_media);
+        } catch (GLib.Error error) {
+            debug("Failed to analyze installer image: %s", error.message);
+
+            var msg = _("Failed to analyze installer media. Corrupted or incomplete media?");
+            App.app.main_window.notificationbar.display_error (msg);
+        }
+    }
+
+    [GtkCallback]
+    private void update_titlebar () {
+        var is_index = (visible_page == index_page);
+        var is_last = (visible_page == review_page);
+
+        next_button.visible = !is_index;
+
+        next_button.label = is_last ? _("Create") : _("Next");
+        previous_button.label = is_index ? _("Cancel") : _("Previous");
+
+        title = visible_page.title;
+    }
+
+    [GtkCallback]
+    private void on_previous_button_clicked () {
+        if (visible_page == index_page)
+            index_page.go_back ();
+        else
+            go_back ();
+    }
+
+    private void go_back () {
+        visible_page.cleanup ();
+
+        pages.set_visible_child (previous_page);
+        if (visible_page.skip)
+            go_back ();
+    }
+
+    [GtkCallback]
+    private void on_next_button_clicked () {
+        visible_page.next ();
+    }
+
+    [GtkCallback]
+    private void do_preparation (Object object) {
+        pages.set_visible_child (preparation_page);
+
+        preparation_page.setup (object as InstallerMedia);
+    }
+
+    [GtkCallback]
+    private void do_setup (Object object) {
+        pages.set_visible_child (setup_page);
+
+        var vm_creator = object as VMCreator;
+        vm_creator.install_media.bind_property ("ready-to-create",
+                                                next_button, "sensitive",
+                                                BindingFlags.SYNC_CREATE);
+
+        setup_page.setup (vm_creator);
+    }
+
+    [GtkCallback]
+    private async void do_review (Object object) {
+        pages.set_visible_child (review_page);
+
+        review_page.setup (object as VMCreator);
+    }
+
+    [GtkCallback]
+    private async void do_create (Object object) {
+        var machine = object as LibvirtMachine;
+
+        var vm_creator = machine.vm_creator;
+        try {
+            vm_creator.launch_vm (machine);
+        } catch (GLib.Error error) {
+            warning ("Failed to create machine: %s", error.message);
+
+            // TODO: launch Notification
+        }
+
+        vm_creator.install_media.clean_up_preparation_cache ();
+
+        shutdown ();
+    }
+
+    public void shutdown () {
+        // TODO: Make the Assistant independent from window states
+        App.app.main_window.set_state (UIState.COLLECTION);
+
+        destroy ();
+    }
+}
diff --git a/src/collection-toolbar.vala b/src/collection-toolbar.vala
index 10fd5b94..281058a9 100644
--- a/src/collection-toolbar.vala
+++ b/src/collection-toolbar.vala
@@ -16,6 +16,8 @@
     [GtkChild]
     private Button new_btn;
     [GtkChild]
+    private MenuButton downloads_hub_btn;
+    [GtkChild]
     private MenuButton hamburger_btn;
     [GtkChild]
     private CollectionFilterSwitcher filter_switcher;
@@ -46,6 +48,8 @@ public void setup_ui (AppWindow window) {
         var builder = new Builder.from_resource ("/org/gnome/Boxes/ui/menus.ui");
         MenuModel menu = (MenuModel) builder.get_object ("app-menu");
         hamburger_btn.popover = new Popover.from_model (hamburger_btn, menu);
+
+        downloads_hub_btn.popover = DownloadsHub.get_instance ();
     }
 
     public void click_back_button () {
@@ -61,13 +65,13 @@ public void click_search_button () {
     }
 
     [GtkCallback]
-    private void on_new_vm_btn_clicked () {
-        window.set_state (UIState.WIZARD);
+    private void on_connect_to_remote_btn_clicked () {
+        window.show_remote_connection_assistant ();
     }
 
     [GtkCallback]
-    private void on_connect_to_remote_btn_clicked () {
-        window.show_remote_connection_assistant ();
+    private void on_create_vm_btn_clicked () {
+        window.show_vm_assistant ();
     }
 
     [GtkCallback]
diff --git a/src/downloads-hub.vala b/src/downloads-hub.vala
new file mode 100644
index 00000000..e4740fd1
--- /dev/null
+++ b/src/downloads-hub.vala
@@ -0,0 +1,129 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/downloads-hub.ui")]
+private class Boxes.DownloadsHub : Gtk.Popover {
+    private static DownloadsHub instance;
+    public static DownloadsHub get_instance () {
+        if (instance == null)
+            instance = new DownloadsHub ();
+
+        return instance;
+    }
+
+    [GtkChild]
+    private ListBox listbox;
+
+    private bool ongoing_downloads {
+        get { return (listbox.get_children ().length () > 0); }
+    }
+
+    // TODO: inhibit suspend
+
+    public void add_item (WizardDownloadableEntry entry) {
+        var row = new DownloadsHubRow.from_entry (entry);
+
+        if (!relative_to.visible)
+            relative_to.visible = true;
+
+        row.destroy.connect (on_row_deleted);
+        row.download_complete.connect (on_download_complete);
+
+        if (!ongoing_downloads) {
+            var reason = _("Downloading media");
+
+            App.app.inhibit (App.app.main_window, null, reason);
+        }
+
+        listbox.prepend (row);
+    }
+
+    private void on_row_deleted () {
+        if (!ongoing_downloads) {
+            // Hide the Downloads Hub when there aren't ongoing downloads
+            relative_to.visible = false;
+        }
+    }
+
+    private void on_download_complete (string label, string path) {
+        var msg = _("“%s“ download complete").printf (label);
+        var notification = new GLib.Notification (msg);
+        notification.add_button (_("Install"), "app.install::" + path);
+
+        App.app.send_notification ("downloaded-" + label, notification);
+
+        if (!ongoing_downloads) {
+            App.app.uninhibit ();
+        }
+    }
+
+    [GtkCallback]
+    private void on_row_activated (Gtk.ListBoxRow _row) {
+        var row = _row as DownloadsHubRow;
+
+        if (row.local_file != null) {
+            App.app.main_window.show_vm_assistant (row.local_file);
+
+            popup ();
+        }
+    }
+}
+
+[GtkTemplate (ui= "/org/gnome/Boxes/ui/downloads-hub-row.ui")]
+private class Boxes.DownloadsHubRow : Gtk.ListBoxRow {
+    [GtkChild]
+    private Label label;
+    [GtkChild]
+    private Image image;
+    [GtkChild]
+    private ProgressBar progress_bar;
+
+    private ActivityProgress progress = new ActivityProgress ();
+    private ulong progress_notify_id;
+
+    private Cancellable cancellable = new Cancellable ();
+
+    public string? local_file;
+
+    public signal void download_complete (string label, string path);
+
+    public DownloadsHubRow.from_entry (WizardDownloadableEntry entry) {
+        label.label = entry.title;
+
+        Downloader.fetch_os_logo.begin (image, entry.os, 64);
+
+        progress_notify_id = progress.notify["progress"].connect (() => {
+            progress_bar.fraction = progress.progress;
+        });
+        progress_bar.fraction = progress.progress = 0;
+
+        var soup_download_uri = new Soup.URI (entry.url);
+        var download_path = soup_download_uri.get_path ();
+
+        var filename = GLib.Path.get_basename (download_path);
+
+        download.begin (entry.url, filename);
+    }
+
+    private async void download (string url, string filename) {
+        try {
+            local_file = yield Downloader.fetch_media (url, filename, progress, cancellable);
+        } catch (IOError.CANCELLED cancel_error) { // We did this, so ignore!
+        } catch (GLib.Error error) {
+            App.app.main_window.notificationbar.display_error (_("Failed to download"));
+
+            warning (error.message);
+            return;
+        }
+
+        download_complete (label.label, local_file);
+    }
+
+    [GtkCallback]
+    private void cancel_download () {
+        progress.disconnect (progress_notify_id);
+        cancellable.cancel ();
+
+        destroy ();
+    }
+}
diff --git a/src/meson.build b/src/meson.build
index 5b59c3b8..c78b8fbd 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -54,6 +54,7 @@ vala_sources = [
   'display-page.vala',
   'display-toolbar.vala',
   'display.vala',
+  'downloads-hub.vala',
   'editable-entry.vala',
   'i-properties-provider.vala',
   'i-collection-view.vala',
@@ -117,7 +118,15 @@ vala_sources = [
   'troubleshoot-log.vala',
   'snapshot-list-row.vala',
   'snapshots-property.vala',
+  'assistant/rhel-download-dialog.vala',
   'assistant/remote-connection.vala',
+  'assistant/vm-assistant.vala',  
+  'assistant/assistant-page.vala',
+  'assistant/index-page.vala',  
+  'assistant/downloads-page.vala',  
+  'assistant/preparation-page.vala',  
+  'assistant/setup-page.vala',  
+  'assistant/review-page.vala',
 ]
 
 dependencies = [
diff --git a/src/util-app.vala b/src/util-app.vala
index 3dc35cf7..2439e77c 100644
--- a/src/util-app.vala
+++ b/src/util-app.vala
@@ -67,6 +67,22 @@ public void widget_remove (Gtk.Widget widget) {
         container.remove (widget);
     }
 
+    public void use_list_box_separator (ListBoxRow row, ListBoxRow? before_row) {
+        if (before_row == null) {
+            row.set_header (null);
+
+            return;
+        }
+
+        var current = row.get_header ();
+        if (current == null) {
+            current = new Separator (Orientation.HORIZONTAL);
+            current.visible = true;
+
+            row.set_header (current);
+        }
+    }
+
     public Osinfo.Device? find_device_by_prop (Osinfo.DeviceList devices, string prop_name, string 
prop_value) {
         var filter = new Osinfo.Filter ();
         filter.add_constraint (prop_name, prop_value);
diff --git a/src/vm-creator.vala b/src/vm-creator.vala
index 8851bd4b..7fe0d7f6 100644
--- a/src/vm-creator.vala
+++ b/src/vm-creator.vala
@@ -3,7 +3,7 @@
 using Osinfo;
 using GVir;
 
-private class Boxes.VMCreator {
+private class Boxes.VMCreator : Object {
     // Seems installers aren't very consistent about exact number of bytes written so we ought to leave some 
margin
     // of error. It's better to report '100%' done while it's not exactly 100% than reporting '99%' done 
forever..
     private const int INSTALL_COMPLETE_PERCENT = 99;


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]