[gnome-boxes/tell-us-what-os-this-is: 2/2] assistant: Let users pick the OS from a list if it is unknown




commit 2ce8db5442cb4641492493866804b43f3c12dc55
Author: Felipe Borges <felipeborges gnome org>
Date:   Fri Jun 26 12:57:50 2020 +0200

    assistant: Let users pick the OS from a list if it is unknown
    
    When we fail to detect an operating system with libosinfo, we often
    go with our default settings. Those work for the majority of OSes
    but not for all. By allowing users to enter which operating system
    they intend to install, we can cover many more installations by
    offering better support for unrecognized medias.
    
    See https://gitlab.gnome.org/Teams/Design/app-mockups/-/raw/master/boxes/boxes-newbox-assistant.png

 data/gnome-boxes.gresource.xml                 |   2 +
 data/ui/assistant/pages/identify-os-page.ui    |  73 ++++++++++++
 data/ui/assistant/pages/identify-os-popover.ui |  37 ++++++
 data/ui/assistant/pages/preparation-page.ui    | 131 +++++++++++----------
 src/assistant/identify-os-page.vala            |  33 ++++++
 src/assistant/identify-os-popover.vala         | 154 +++++++++++++++++++++++++
 src/assistant/preparation-page.vala            |  29 ++++-
 src/media-manager.vala                         |   5 +-
 src/meson.build                                |   2 +
 src/os-database.vala                           |  20 ++++
 10 files changed, 422 insertions(+), 64 deletions(-)
---
diff --git a/data/gnome-boxes.gresource.xml b/data/gnome-boxes.gresource.xml
index 3fe6f967..33f344cb 100644
--- a/data/gnome-boxes.gresource.xml
+++ b/data/gnome-boxes.gresource.xml
@@ -54,6 +54,8 @@
     <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/identify-os-page.ui</file>
+    <file preprocess="xml-stripblanks">ui/assistant/pages/identify-os-popover.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>
diff --git a/data/ui/assistant/pages/identify-os-page.ui b/data/ui/assistant/pages/identify-os-page.ui
new file mode 100644
index 00000000..742de4ca
--- /dev/null
+++ b/data/ui/assistant/pages/identify-os-page.ui
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="BoxesIdentifyOsPage" parent="GtkBox">
+    <property name="visible">True</property>
+    <property name="visible">True</property>
+    <property name="orientation">vertical</property>
+    <property name="border-width">60</property>
+    <property name="spacing">50</property>
+    <property name="halign">center</property>
+
+    <child>
+      <object class="GtkLabel">
+        <property name="visible">True</property>
+        <property name="wrap">True</property>
+        <property name="xalign">0</property>
+        <property name="halign">start</property>
+        <property name="max-width-chars">80</property>
+        <property name="label" translatable="yes">Boxes was unable to identify the operating system on the 
image file.</property>
+      </object>
+    </child>
+
+    <child>
+      <object class="GtkLabel">
+        <property name="visible">True</property>
+        <property name="wrap">True</property>
+        <property name="xalign">0</property>
+        <property name="halign">start</property>
+        <property name="max-width-chars">60</property>
+        <property name="label" translatable="yes">It is not advised to try booting an unknown OS, but you 
may try your luck forcing a specific kind of OS from the list below:</property>
+      </object>
+    </child>
+
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="spacing">20</property>
+
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">Template</property>
+          </object>
+        </child>
+
+        <child>
+          <object class="GtkMenuButton" id="menu_button">
+            <property name="visible">True</property>
+
+            <child>
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <child>
+                  <object class="GtkLabel" id="menu_button_label">
+                    <property name="visible">True</property>
+                    <property name="hexpand">True</property>
+                    <property name="halign">start</property>
+                    <property name="label" translatable="yes">Unknown OS</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">True</property>
+                    <property name="icon-name">go-down-symbolic</property>
+                  </object>
+                </child>
+              </object>>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/ui/assistant/pages/identify-os-popover.ui b/data/ui/assistant/pages/identify-os-popover.ui
new file mode 100644
index 00000000..61094732
--- /dev/null
+++ b/data/ui/assistant/pages/identify-os-popover.ui
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="BoxesIdentifyOsPopover" parent="GtkPopover">
+    <property name="modal">True</property>
+    <property name="position">bottom</property>
+    <property name="can_focus">False</property>
+    <property name="width-request">450</property>
+
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="border-width">5</property>
+        <property name="spacing">10</property>
+        <property name="orientation">vertical</property>
+
+        <child>
+          <object class="GtkSearchEntry" id="search_entry">
+            <property name="visible">True</property>
+            <property name="placeholder-text" translatable="yes">Search for an OS…</property>
+            <signal name="search-changed" handler="on_search_entry_changed" />
+          </object>
+        </child>
+
+        <child>
+          <object class="GtkListBox" id="listbox">
+            <property name="visible">True</property>
+            <property name="selection-mode">single  </property>
+            <signal name="row-selected" handler="on_listbox_row_selected"/>
+            <signal name="row-activated" handler="on_listbox_row_activated"/>
+          </object>
+        </child>
+
+      </object>
+    </child>
+
+  </template>
+</interface>
diff --git a/data/ui/assistant/pages/preparation-page.ui b/data/ui/assistant/pages/preparation-page.ui
index 9ddec630..388113b8 100644
--- a/data/ui/assistant/pages/preparation-page.ui
+++ b/data/ui/assistant/pages/preparation-page.ui
@@ -5,79 +5,92 @@
     <property name="title" translatable="yes">Preparing…</property>
 
     <child>
-      <object class="GtkGrid">
+      <object class="GtkStack" id="stack">
         <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">
+          <object class="GtkGrid">
             <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>
+            <property name="expand">True</property>
+            <property name="halign">center</property>
+            <property name="valign">center</property>
+            <property name="column-spacing">10</property>
 
-        <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">
+                <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="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="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="status_label">
-            <property name="visible">True</property>
-            <property name="halign">start</property>
-            <property name="valign">center</property>
+            <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>
-          <packing>
-            <property name="left-attach">1</property>
-            <property name="top-attach">2</property>
-          </packing>
         </child>
 
         <child>
-          <object class="GtkProgressBar" id="progress_bar">
+          <object class="BoxesIdentifyOsPage" id="identify_os_page">
             <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>
diff --git a/src/assistant/identify-os-page.vala b/src/assistant/identify-os-page.vala
new file mode 100644
index 00000000..151d57b6
--- /dev/null
+++ b/src/assistant/identify-os-page.vala
@@ -0,0 +1,33 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/pages/identify-os-page.ui")]
+public class Boxes.IdentifyOsPage : Gtk.Box {
+    private Boxes.IdentifyOsPopover os_popover;
+
+    [GtkChild]
+    private Gtk.MenuButton menu_button;
+    [GtkChild]
+    private Gtk.Label menu_button_label;
+
+    private Osinfo.Os? selected_os = null;
+
+    construct {
+        os_popover = new Boxes.IdentifyOsPopover ();
+        os_popover.os_selected.connect (on_os_selected);
+
+        menu_button.popover = os_popover;
+    }
+
+    public Osinfo.Os? get_selected_os () {
+        return selected_os;
+    }
+
+    private void on_os_selected (Osinfo.Os? os) {
+        this.selected_os = os;
+
+        if (selected_os != null)
+            menu_button_label.set_label (os.get_name ());
+        else
+            menu_button_label.set_label (_("Unknown OS"));
+    }
+}
diff --git a/src/assistant/identify-os-popover.vala b/src/assistant/identify-os-popover.vala
new file mode 100644
index 00000000..f8d2b471
--- /dev/null
+++ b/src/assistant/identify-os-popover.vala
@@ -0,0 +1,154 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/pages/identify-os-popover.ui")]
+public class Boxes.IdentifyOsPopover : Gtk.Popover {
+    [GtkChild]
+    private Gtk.SearchEntry search_entry;
+    [GtkChild]
+    private Gtk.ListBox listbox;
+    private GLib.ListStore model;
+
+    private GLib.List<weak Osinfo.Entity> os_list;
+
+    public signal void os_selected (Osinfo.Os? os);
+
+    private Gtk.ListBoxRow? previous_row = null;
+
+    public IdentifyOsPopover () {
+        search_entry.grab_focus ();
+
+        setup_model.begin ();
+    }
+
+    private async void setup_model () {
+        var media_manager = MediaManager.get_instance ();
+
+        os_list = yield media_manager.os_db.get_all_oses_sorted_by_release_date ();
+        model = new GLib.ListStore (typeof (Osinfo.Os));
+
+        yield purge_model ();
+        listbox.bind_model (model, create_listbox_entry);
+    }
+
+    private Gtk.Widget create_listbox_entry (Object item) {
+        var os_item = item as Osinfo.Os;
+
+        return new OsListEntry (os_item);
+    }
+
+    private async void purge_model () {
+        model.remove_all ();
+
+        foreach (var media in yield get_recommended_downloads ()) {
+            if (media != null) {
+                model.append (media.os);
+            }
+        }
+    }
+
+    [GtkCallback]
+    private async void on_search_entry_changed () {
+        var text = search_entry.get_text ();
+        if (text.length == 0) {
+            yield purge_model ();
+
+            return;
+        } else {
+            model.remove_all ();
+        }
+
+        listbox.select_row (null);
+
+        var query = canonicalize_for_search (text);
+        var nresults = 6;
+        foreach (var entity in os_list) {
+            if (nresults == 0)
+                return;
+
+            var os = entity as Osinfo.Os;
+            var os_name = os.get_name ();
+            if (os_name == null)
+                continue;
+
+            var name = canonicalize_for_search (os_name);
+            if (query in name) {
+                model.append (os);
+
+                nresults -= 1;
+            }
+        }
+    }
+
+    [GtkCallback]
+    private void on_listbox_row_activated (Gtk.ListBoxRow row) {
+        if (row != previous_row) {
+            previous_row = row;
+
+            return;
+        }
+
+        listbox.unselect_row (row);
+        previous_row = null;
+
+        var child = row.get_child () as OsListEntry;
+        child.selected = false;
+
+        os_selected (null);
+    }
+
+    [GtkCallback]
+    private void on_listbox_row_selected () {
+        var row = listbox.get_selected_row ();
+        if (row == null)
+            return;
+
+        foreach (var widget in listbox.get_children ()) {
+            var entry = widget as Gtk.ListBoxRow;
+            var child = entry.get_child () as OsListEntry;
+
+            child.selected = false;
+        }
+
+        var os_list_entry = row.get_child () as OsListEntry;
+        os_list_entry.selected = true;
+
+        os_selected (os_list_entry.os);
+    }
+}
+
+class OsListEntry : Gtk.Box {
+    public Osinfo.Os os;
+
+    public bool selected {
+        set {
+            image.visible = value;
+        }
+
+        get {
+            return image.visible;
+        }
+    }
+
+    private Gtk.Label label = new Gtk.Label (null) {
+        margin = 5,
+        halign = Gtk.Align.START,
+        xalign = 0,
+        visible = true
+    };
+
+    private Gtk.Image image = new Gtk.Image.from_icon_name ("object-select-symbolic", Gtk.IconSize.MENU);
+
+    public OsListEntry (Osinfo.Os os) {
+        this.os = os;
+
+        var os_name = os.get_name ();
+        if (os_name != null) {
+            label.label = os_name.replace ("Unknown", "");
+            visible = true;
+        }
+        add (label);
+
+        add (image);
+        image.visible = false;
+    }
+}
diff --git a/src/assistant/preparation-page.vala b/src/assistant/preparation-page.vala
index 2579284a..84e0670d 100644
--- a/src/assistant/preparation-page.vala
+++ b/src/assistant/preparation-page.vala
@@ -2,6 +2,11 @@
 
 [GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/pages/preparation-page.ui")]
 private class Boxes.AssistantPreparationPage : AssistantPage {
+    [GtkChild]
+    private Gtk.Stack stack;
+    [GtkChild]
+    private Boxes.IdentifyOsPage identify_os_page;
+
     [GtkChild]
     private Gtk.Label media_label;
     [GtkChild]
@@ -26,20 +31,36 @@
         }
     }
 
-    public void setup (InstallerMedia media) {
+    public void setup (InstallerMedia media, Osinfo.Os? os = null) {
         try {
             var media_manager = MediaManager.get_instance ();
-            media = media_manager.create_installer_media_from_media (media);
+            this.media = media_manager.create_installer_media_from_media (media, os);
+
         } catch (GLib.Error error) {
             warning ("Failed to setup installation media '%s': %s", media.device_file, error.message);
         }
 
-        prepare.begin (media);
+        if (media.os == null) {
+            stack.visible_child = identify_os_page;
+        } else {
+            prepare.begin (media);
+        }
+    }
+
+    public async override void next () {
+        var media_manager = MediaManager.get_instance ();
+        var os = identify_os_page.get_selected_os ();
+        if (os == null) {
+            prepare.begin (media);
 
-        skip = true;
+            return;
+        }
+
+        setup (media, os);
     }
 
     public async void prepare (InstallerMedia media) {
+        skip = true;
         var progress = create_preparation_progress ();
         if (!yield media.prepare (progress, cancellable)) // add cancellable
             return;
diff --git a/src/media-manager.vala b/src/media-manager.vala
index fee4b4c3..79b198c3 100644
--- a/src/media-manager.vala
+++ b/src/media-manager.vala
@@ -206,7 +206,10 @@ private static InstallerMedia create_unattended_installer (InstallerMedia media)
         return install_media;
     }
 
-    public InstallerMedia create_installer_media_from_media (InstallerMedia media) throws GLib.Error {
+    public InstallerMedia create_installer_media_from_media (InstallerMedia media, Os? os = null) throws 
GLib.Error {
+        if (os != null)
+            media.os = os;
+
         if (media.os == null)
             return media;
 
diff --git a/src/meson.build b/src/meson.build
index 3d4302af..63006644 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -119,6 +119,8 @@ vala_sources = [
   'assistant/vm-assistant.vala',  
   'assistant/assistant-page.vala',
   'assistant/index-page.vala',  
+  'assistant/identify-os-page.vala',
+  'assistant/identify-os-popover.vala',
   'assistant/downloads-page.vala',  
   'assistant/preparation-page.vala',  
   'assistant/setup-page.vala',  
diff --git a/src/os-database.vala b/src/os-database.vala
index 41fd8e70..5f14bc76 100644
--- a/src/os-database.vala
+++ b/src/os-database.vala
@@ -101,6 +101,26 @@ public async Os get_os_by_id (string id) throws OSDatabaseError {
         return os;
     }
 
+    public async GLib.List<weak Osinfo.Entity> get_all_oses_sorted_by_release_date () {
+        if (!yield ensure_db_loaded ())
+            throw new OSDatabaseError.DB_LOADING_FAILED ("Failed to load OS database");
+
+        var os_list = db.get_os_list ().get_elements ();
+        os_list.sort ((os_a, os_b) => {
+            var release_a = (os_a as Os).get_release_date ();
+            var release_b = (os_b as Os).get_release_date ();
+
+            if (release_a == null)
+                return -1;
+            else if (release_b == null)
+                return 1;
+
+            return release_b.compare (release_a);
+        });
+
+        return os_list;
+    }
+
     public async GLib.List<Osinfo.Media> list_downloadable_oses () throws OSDatabaseError {
         if (!yield ensure_db_loaded ())
             throw new OSDatabaseError.DB_LOADING_FAILED ("Failed to load OS database");


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