[virt-tools-list] [virt-manager PATCH v4] Redesign OS distro selection UI to be faster to use

Daniel P. Berrangé berrange at redhat.com
Tue May 1 11:51:23 UTC 2018


The current OS distro selection UI is fairly cumbersome to use. First
you need to decide on a variant, then decide a distro and then look for
the version you want. The list is filtered by default so only a subset
of OS are displayed. So for less common distros you'll then need to
start again and tell it to show all OS to try to find the one you want.

The core problem is that we have an incredibly large list and want to
make it easy for the user to find a specific entry. The modern UI
paradigm for this problem is to provide interactive search with
live updated results. The current UI does provide an interactive search
facility on the OS version results, but you still have to first select a
variant to be able to use the search which is unhelpful.

This patch attempts to better apply the search UI design to the OS selection
problem. We get rid of the notion of variants, distros and version, and
provide a single text entry box in which the user can type a few letters
of the OS name. As they type, a popover displays the matching results
filtered on OS name. By default end of life OS will be hidden, so in
general there will only be a small handful of results left after just
typing a few characters. This makes it very quick to find and select the
desired OS, without needing to provide a mutli-step navigation hierarchy.

https://bugzilla.redhat.com/show_bug.cgi?id=1464306

Signed-off-by: Daniel P. Berrangé <berrange at redhat.com>
---
 tests/osdict.py       |  26 ----
 ui/create.ui          | 256 +++++++-----------------------
 ui/oslist.ui          | 104 +++++++++++++
 virtManager/create.py | 422 +++++++++-----------------------------------------
 virtManager/oslist.py | 106 +++++++++++++
 virtinst/osdict.py    |  88 +++--------
 6 files changed, 361 insertions(+), 641 deletions(-)
 create mode 100644 ui/oslist.ui
 create mode 100644 virtManager/oslist.py

diff --git a/tests/osdict.py b/tests/osdict.py
index 62d3ca7b..4c299962 100644
--- a/tests/osdict.py
+++ b/tests/osdict.py
@@ -45,29 +45,3 @@ class TestOSDB(unittest.TestCase):
         guest.type = "qemu"
         res = OSDB.lookup_os("fedora21").get_recommended_resources(guest)
         assert res["n-cpus"] == 1
-
-    def test_list_os(self):
-        full_list = OSDB.list_os()
-        pref_list = OSDB.list_os(typename="linux", sortpref=["fedora", "rhel"])
-        support_list = OSDB.list_os(only_supported=True)
-
-        assert full_list[0] is not pref_list[0]
-        assert len(full_list) > len(support_list)
-        assert len(OSDB.list_os(typename="generic")) == 1
-
-        # Verify that sort order actually worked
-        found_fedora = False
-        found_rhel = False
-        for idx, osobj in enumerate(pref_list[:]):
-            if osobj.name.startswith("fedora"):
-                found_fedora = True
-                continue
-
-            for osobj2 in pref_list[idx:]:
-                if osobj2.name.startswith("rhel"):
-                    found_rhel = True
-                    continue
-                break
-            break
-
-        assert found_fedora and found_rhel
diff --git a/ui/create.ui b/ui/create.ui
index 532c0331..cfb449b3 100644
--- a/ui/create.ui
+++ b/ui/create.ui
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.20.3 -->
+<!-- Generated with glade 3.20.4 -->
 <interface>
   <requires lib="gtk+" version="3.14"/>
   <object class="GtkAdjustment" id="adjustment2">
@@ -18,8 +18,8 @@
     <property name="stock">gtk-new</property>
   </object>
   <object class="GtkWindow" id="vmm-create">
-    <property name="width_request">400</property>
-    <property name="height_request">400</property>
+    <property name="width_request">600</property>
+    <property name="height_request">500</property>
     <property name="can_focus">False</property>
     <property name="title" translatable="yes">New VM</property>
     <property name="resizable">False</property>
@@ -2027,184 +2027,89 @@ connections is not yet supported.</small></property>
                           </packing>
                         </child>
                         <child>
-                          <object class="GtkBox" id="install-os-distro-box">
+                          <object class="GtkFrame" id="install-os-distro-box">
                             <property name="visible">True</property>
                             <property name="can_focus">False</property>
-                            <property name="orientation">vertical</property>
-                            <property name="spacing">6</property>
+                            <property name="label_xalign">0</property>
+                            <property name="shadow_type">none</property>
                             <child>
-                              <object class="GtkBox" id="install-detect-os-box">
+                              <object class="GtkAlignment">
                                 <property name="visible">True</property>
                                 <property name="can_focus">False</property>
-                                <property name="orientation">vertical</property>
-                                <signal name="hide" handler="on_install_detect_os_box_hide" swapped="no"/>
-                                <signal name="show" handler="on_install_detect_os_box_show" swapped="no"/>
+                                <property name="top_padding">6</property>
+                                <property name="bottom_padding">6</property>
+                                <property name="left_padding">12</property>
                                 <child>
-                                  <object class="GtkCheckButton" id="install-detect-os">
-                                    <property name="label" translatable="yes">A_utomatically detect operating system based on install media</property>
-                                    <property name="visible">True</property>
-                                    <property name="can_focus">True</property>
-                                    <property name="receives_default">False</property>
-                                    <property name="use_underline">True</property>
-                                    <property name="active">True</property>
-                                    <property name="draw_indicator">True</property>
-                                    <signal name="toggled" handler="on_install_detect_os_toggled" swapped="no"/>
-                                  </object>
-                                  <packing>
-                                    <property name="expand">False</property>
-                                    <property name="fill">True</property>
-                                    <property name="position">0</property>
-                                  </packing>
-                                </child>
-                              </object>
-                              <packing>
-                                <property name="expand">False</property>
-                                <property name="fill">False</property>
-                                <property name="position">0</property>
-                              </packing>
-                            </child>
-                            <child>
-                              <object class="GtkLabel" id="install-nodetect-label">
-                                <property name="can_focus">False</property>
-                                <property name="halign">start</property>
-                                <property name="label" translatable="yes">Choose an operating system type and version</property>
-                              </object>
-                              <packing>
-                                <property name="expand">False</property>
-                                <property name="fill">False</property>
-                                <property name="position">1</property>
-                              </packing>
-                            </child>
-                            <child>
-                              <object class="GtkAlignment" id="alignment9">
-                                <property name="visible">True</property>
-                                <property name="can_focus">False</property>
-                                <property name="left_padding">15</property>
-                                <child>
-                                  <object class="GtkGrid" id="table1">
+                                  <object class="GtkBox">
                                     <property name="visible">True</property>
                                     <property name="can_focus">False</property>
-                                    <property name="row_spacing">4</property>
-                                    <property name="column_spacing">6</property>
-                                    <child>
-                                      <object class="GtkLabel" id="install-os-version-label">
-                                        <property name="visible">True</property>
-                                        <property name="can_focus">False</property>
-                                        <property name="halign">start</property>
-                                        <property name="valign">center</property>
-                                        <property name="label">-</property>
-                                        <child internal-child="accessible">
-                                          <object class="AtkObject" id="install-os-version-label-atkobject">
-                                            <property name="AtkObject::accessible-name">install-os-version-label</property>
-                                          </object>
-                                        </child>
-                                      </object>
-                                      <packing>
-                                        <property name="left_attach">2</property>
-                                        <property name="top_attach">1</property>
-                                      </packing>
-                                    </child>
-                                    <child>
-                                      <object class="GtkLabel" id="install-os-type-label">
-                                        <property name="visible">True</property>
-                                        <property name="can_focus">False</property>
-                                        <property name="halign">start</property>
-                                        <property name="valign">center</property>
-                                        <property name="label">-</property>
-                                        <child internal-child="accessible">
-                                          <object class="AtkObject" id="install-os-type-label-atkobject">
-                                            <property name="AtkObject::accessible-name">install-os-type-label</property>
-                                          </object>
-                                        </child>
-                                      </object>
-                                      <packing>
-                                        <property name="left_attach">2</property>
-                                        <property name="top_attach">0</property>
-                                      </packing>
-                                    </child>
-                                    <child>
-                                      <object class="GtkComboBox" id="install-os-type">
-                                        <property name="visible">True</property>
-                                        <property name="can_focus">False</property>
-                                        <property name="valign">center</property>
-                                        <signal name="changed" handler="on_install_os_type_changed" swapped="no"/>
-                                        <child internal-child="accessible">
-                                          <object class="AtkObject" id="install-os-type-atkobject">
-                                            <property name="AtkObject::accessible-name">install-os-type</property>
-                                          </object>
-                                        </child>
-                                      </object>
-                                      <packing>
-                                        <property name="left_attach">1</property>
-                                        <property name="top_attach">0</property>
-                                      </packing>
-                                    </child>
-                                    <child>
-                                      <object class="GtkLabel" id="label17">
-                                        <property name="visible">True</property>
-                                        <property name="can_focus">False</property>
-                                        <property name="halign">start</property>
-                                        <property name="valign">center</property>
-                                        <property name="label" translatable="yes">_Version:</property>
-                                        <property name="use_underline">True</property>
-                                        <property name="mnemonic_widget">install-os-version</property>
-                                      </object>
-                                      <packing>
-                                        <property name="left_attach">0</property>
-                                        <property name="top_attach">1</property>
-                                      </packing>
-                                    </child>
+                                    <property name="orientation">vertical</property>
+                                    <property name="spacing">6</property>
                                     <child>
-                                      <object class="GtkLabel" id="label16">
+                                      <object class="GtkSearchEntry" id="install-os-name">
                                         <property name="visible">True</property>
-                                        <property name="can_focus">False</property>
-                                        <property name="halign">end</property>
-                                        <property name="valign">center</property>
-                                        <property name="label" translatable="yes">OS _type:</property>
-                                        <property name="use_underline">True</property>
-                                        <property name="mnemonic_widget">install-os-type</property>
+                                        <property name="can_focus">True</property>
+                                        <property name="primary_icon_name">edit-find-symbolic</property>
+                                        <property name="primary_icon_activatable">False</property>
+                                        <property name="primary_icon_sensitive">False</property>
+                                        <signal name="search-changed" handler="on_install_os_name_search_changed" swapped="no"/>
+                                        <signal name="stop-search" handler="on_install_os_name_stop_search" swapped="no"/>
                                       </object>
                                       <packing>
-                                        <property name="left_attach">0</property>
-                                        <property name="top_attach">0</property>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="position">0</property>
                                       </packing>
                                     </child>
                                     <child>
-                                      <object class="GtkComboBox" id="install-os-version">
+                                      <object class="GtkBox" id="install-detect-os-box">
                                         <property name="visible">True</property>
                                         <property name="can_focus">False</property>
-                                        <property name="valign">center</property>
-                                        <property name="has_entry">True</property>
-                                        <signal name="changed" handler="on_install_os_version_changed" swapped="no"/>
-                                        <child internal-child="entry">
-                                          <object class="GtkEntry" id="install-os-version-entry">
+                                        <child>
+                                          <object class="GtkCheckButton" id="install-detect-os">
+                                            <property name="label" translatable="yes">A_utomatically detect from the installation media / source</property>
+                                            <property name="visible">True</property>
                                             <property name="can_focus">True</property>
-                                            <child internal-child="accessible">
-                                              <object class="AtkObject" id="install-os-version-entry-atkobject">
-                                                <property name="AtkObject::accessible-name">install-os-version-entry</property>
-                                              </object>
-                                            </child>
+                                            <property name="receives_default">False</property>
+                                            <property name="use_underline">True</property>
+                                            <property name="active">True</property>
+                                            <property name="draw_indicator">True</property>
+                                            <signal name="toggled" handler="on_install_detect_os_toggled" swapped="no"/>
                                           </object>
+                                          <packing>
+                                            <property name="expand">False</property>
+                                            <property name="fill">True</property>
+                                            <property name="position">0</property>
+                                          </packing>
                                         </child>
-                                        <child internal-child="accessible">
-                                          <object class="AtkObject" id="install-os-version-atkobject">
-                                            <property name="AtkObject::accessible-name">install-os-version</property>
+                                        <child>
+                                          <object class="GtkSpinner" id="install-detect-os-spinner">
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">False</property>
                                           </object>
+                                          <packing>
+                                            <property name="expand">False</property>
+                                            <property name="fill">True</property>
+                                            <property name="position">1</property>
+                                          </packing>
                                         </child>
                                       </object>
                                       <packing>
-                                        <property name="left_attach">1</property>
-                                        <property name="top_attach">1</property>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="position">1</property>
                                       </packing>
                                     </child>
                                   </object>
                                 </child>
                               </object>
-                              <packing>
-                                <property name="expand">False</property>
-                                <property name="fill">False</property>
-                                <property name="position">2</property>
-                              </packing>
+                            </child>
+                            <child type="label">
+                              <object class="GtkLabel">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="label" translatable="yes">Operating system distribution:</property>
+                              </object>
                             </child>
                           </object>
                           <packing>
@@ -2753,50 +2658,6 @@ connections is not yet supported.</small></property>
                             <property name="position">0</property>
                           </packing>
                         </child>
-                        <child>
-                          <object class="GtkAlignment" id="finish-warn-os-align">
-                            <property name="visible">True</property>
-                            <property name="can_focus">False</property>
-                            <property name="left_padding">6</property>
-                            <child>
-                              <object class="GtkBox" id="finish-warn-os">
-                                <property name="visible">True</property>
-                                <property name="can_focus">False</property>
-                                <property name="spacing">3</property>
-                                <child>
-                                  <object class="GtkImage" id="image4">
-                                    <property name="visible">True</property>
-                                    <property name="can_focus">False</property>
-                                    <property name="stock">gtk-dialog-warning</property>
-                                  </object>
-                                  <packing>
-                                    <property name="expand">False</property>
-                                    <property name="fill">True</property>
-                                    <property name="position">0</property>
-                                  </packing>
-                                </child>
-                                <child>
-                                  <object class="GtkLabel" id="label47">
-                                    <property name="visible">True</property>
-                                    <property name="can_focus">False</property>
-                                    <property name="label" translatable="yes"><small>Specifying an operating system is required for best performance</small></property>
-                                    <property name="use_markup">True</property>
-                                  </object>
-                                  <packing>
-                                    <property name="expand">False</property>
-                                    <property name="fill">True</property>
-                                    <property name="position">1</property>
-                                  </packing>
-                                </child>
-                              </object>
-                            </child>
-                          </object>
-                          <packing>
-                            <property name="expand">False</property>
-                            <property name="fill">True</property>
-                            <property name="position">1</property>
-                          </packing>
-                        </child>
                         <child>
                           <object class="GtkExpander" id="advanced-expander">
                             <property name="visible">True</property>
@@ -2884,14 +2745,13 @@ connections is not yet supported.</small></property>
                                 <property name="can_focus">False</property>
                                 <property name="label" translatable="yes">N_etwork selection</property>
                                 <property name="use_underline">True</property>
-                                <property name="mnemonic_widget">advanced-expander</property>
                               </object>
                             </child>
                           </object>
                           <packing>
                             <property name="expand">False</property>
                             <property name="fill">False</property>
-                            <property name="position">2</property>
+                            <property name="position">1</property>
                           </packing>
                         </child>
                       </object>
diff --git a/ui/oslist.ui b/ui/oslist.ui
new file mode 100644
index 00000000..03148545
--- /dev/null
+++ b/ui/oslist.ui
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.4 -->
+<interface>
+  <requires lib="gtk+" version="3.14"/>
+  <object class="GtkPopover" id="vmm-oslist">
+    <property name="width_request">400</property>
+    <property name="height_request">300</property>
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_left">6</property>
+        <property name="margin_right">6</property>
+        <property name="margin_top">6</property>
+        <property name="margin_bottom">6</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">6</property>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="spacing">6</property>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="stock">gtk-info</property>
+                <property name="icon_size">3</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">Can't find the operating system you are looking for ?
+Try selecting the next most recent version displayed,
+or use the "Generic" entry.</property>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkScrolledWindow">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="vscrollbar_policy">always</property>
+            <property name="shadow_type">in</property>
+            <child>
+              <object class="GtkTreeView" id="os-list">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="headers_visible">False</property>
+                <property name="enable_search">False</property>
+                <property name="hover_selection">True</property>
+                <property name="enable_grid_lines">horizontal</property>
+                <property name="activate_on_single_click">True</property>
+                <child internal-child="selection">
+                  <object class="GtkTreeSelection"/>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkCheckButton" id="include-eol">
+            <property name="label" translatable="yes">Include end of life operating systems</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">False</property>
+            <property name="halign">start</property>
+            <property name="draw_indicator">True</property>
+            <signal name="toggled" handler="on_include_eol_toggled" swapped="no"/>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/virtManager/create.py b/virtManager/create.py
index 5afe0afe..c3f14bdf 100644
--- a/virtManager/create.py
+++ b/virtManager/create.py
@@ -29,6 +29,7 @@ from .engine import vmmEngine
 from .mediacombo import vmmMediaCombo
 from .netlist import vmmNetworkList
 from .storagebrowse import vmmStorageBrowser
+from .oslist import vmmOSList
 
 # Number of seconds to wait for media detection
 DETECT_TIMEOUT = 20
@@ -124,6 +125,7 @@ class vmmCreate(vmmGObjectUI):
 
         self._guest = None
         self._failed_guest = None
+        self._os = None
 
         # Distro detection state variables
         self._detect_os_in_progress = False
@@ -171,10 +173,6 @@ class vmmCreate(vmmGObjectUI):
             "on_install_container_source_toggle": self._container_source_toggle,
 
             "on_install_detect_os_toggled": self._toggle_detect_os,
-            "on_install_os_type_changed": self._change_os_type,
-            "on_install_os_version_changed": self._change_os_version,
-            "on_install_detect_os_box_show": self._os_detect_visibility_changed,
-            "on_install_detect_os_box_hide": self._os_detect_visibility_changed,
 
             "on_kernel_browse_clicked": self._browse_kernel,
             "on_initrd_browse_clicked": self._browse_initrd,
@@ -183,6 +181,9 @@ class vmmCreate(vmmGObjectUI):
             "on_enable_storage_toggled": self._toggle_enable_storage,
 
             "on_create_vm_name_changed": self._name_changed,
+
+            "on_install_os_name_search_changed": self._os_name_search_changed,
+            "on_install_os_name_stop_search": self._os_name_stop_search,
         })
         self.bind_escape_key_close()
 
@@ -294,46 +295,6 @@ class vmmCreate(vmmGObjectUI):
         # Lists for OS container bootstrap
         set_model_list("install-oscontainer-source-url-combo")
 
-        def sep_func(model, it, combo):
-            ignore = combo
-            return model[it][OS_COL_IS_SEP]
-
-        def make_os_model():
-            # [os value, os label, is seperator, is 'show all']
-            cols = []
-            cols.insert(OS_COL_ID, str)
-            cols.insert(OS_COL_LABEL, str)
-            cols.insert(OS_COL_IS_SEP, bool)
-            cols.insert(OS_COL_IS_SHOW_ALL, bool)
-            return Gtk.TreeStore(*cols)
-
-        def make_completion_model():
-            # [os value, os label]
-            cols = []
-            cols.insert(OS_COL_ID, str)
-            cols.insert(OS_COL_LABEL, str)
-            return Gtk.ListStore(*cols)
-
-        # Lists for distro type + variant
-        os_type_list = self.widget("install-os-type")
-        os_type_model = make_os_model()
-        os_type_list.set_model(os_type_model)
-        uiutil.init_combo_text_column(os_type_list, 1)
-        os_type_list.set_row_separator_func(sep_func, os_type_list)
-
-        os_variant_list = self.widget("install-os-version")
-        os_variant_model = make_os_model()
-        os_variant_list.set_model(os_variant_model)
-        uiutil.init_combo_text_column(os_variant_list, 1)
-        os_variant_list.set_row_separator_func(sep_func, os_variant_list)
-
-        entry = self.widget("install-os-version-entry")
-        completion = Gtk.EntryCompletion()
-        entry.set_completion(completion)
-        completion.set_text_column(1)
-        completion.set_inline_completion(True)
-        completion.set_model(make_completion_model())
-
         # Archtecture
         archList = self.widget("arch")
         # [label, guest.os.arch value]
@@ -365,6 +326,16 @@ class vmmCreate(vmmGObjectUI):
         lst.set_model(model)
         uiutil.init_combo_text_column(lst, 0)
 
+        self._os_list = vmmOSList()
+        self._os_list.connect("os-selected", self._os_name_selected)
+
+    def _os_name_selected(self, ignore, osobj):
+        name = self.widget("install-os-name")
+        self._os = osobj
+        self._os_list.hide()
+
+        if self._os is not None:
+            name.set_text(self._os.label)
 
     def _reset_state(self, urihint=None):
         """
@@ -395,8 +366,6 @@ class vmmCreate(vmmGObjectUI):
 
         # Distro/Variant
         self._toggle_detect_os(self.widget("install-detect-os"))
-        self._populate_os_type_model()
-        self.widget("install-os-type").set_active(0)
 
         def _populate_media_model(media_model, urls):
             media_model.clear()
@@ -415,7 +384,6 @@ class vmmCreate(vmmGObjectUI):
         self.widget("install-url-options").set_expanded(False)
         urlmodel = self.widget("install-url-combo").get_model()
         _populate_media_model(urlmodel, self.config.get_media_urls())
-        self._set_distro_labels("-", "-")
 
         # Install import
         self.widget("install-import-entry").set_text("")
@@ -970,183 +938,6 @@ class vmmCreate(vmmGObjectUI):
     # Helpers for populating OS type/variant UI #
     #############################################
 
-    def _add_os_row(self, model, name="", label="",
-                      sep=False, action=False, parent=None):
-        """
-        Helper for building an os type/version row and adding it to
-        the list model if necessary
-        """
-
-        row = []
-        row.insert(OS_COL_ID, name)
-        row.insert(OS_COL_LABEL, label)
-        row.insert(OS_COL_IS_SEP, sep)
-        row.insert(OS_COL_IS_SHOW_ALL, action)
-
-        return model.append(parent, row)
-
-    def _add_completion_row(self, model, name, label):
-        row = []
-        row.insert(OS_COL_ID, name)
-        row.insert(OS_COL_LABEL, label)
-        model.append(row)
-
-    def _populate_os_type_model(self):
-        widget = self.widget("install-os-type")
-        model = widget.get_model()
-        model.clear()
-
-        # Kind of a hack, just show linux + windows by default since
-        # that's all 98% of people care about
-        supported = {"generic", "linux", "windows"}
-
-        # Move 'generic' to the front of the list
-        types = virtinst.OSDB.list_types()
-        types.remove("generic")
-        types.insert(0, "generic")
-
-        # Pretty names for OSes.  If a new OS is not found here,
-        # its capitalized name is used.
-        oses = {
-            "bsd": _("BSD"),
-            "generic": _("Generic"),
-            "linux": _("Linux"),
-            "macos": _("macOS"),
-            "other": _("Others"),
-            "solaris": _("Solaris"),
-            "windows": _("Windows"),
-        }
-
-        # When only the "supported" types are requested,
-        # filter them.
-        if not self._show_all_os_was_selected:
-            types = [t for t in types if t in supported]
-
-        for typename in types:
-            try:
-                typelabel = oses[typename]
-            except KeyError:
-                typelabel = typename.capitalize()
-
-            self._add_os_row(model, typename, typelabel)
-
-        if not self._show_all_os_was_selected:
-            self._add_os_row(model, sep=True)
-            self._add_os_row(model, label=_("Show all OS options"), action=True)
-
-        # Select 'generic' by default
-        widget.set_active(0)
-
-    def _populate_os_variant_model(self, _type):
-        widget = self.widget("install-os-version")
-        model = widget.get_model()
-        model.clear()
-
-        completion_model = self.widget("install-os-version-entry").get_completion().get_model()
-        completion_model.clear()
-
-        preferred = self.config.preferred_distros
-
-        # All the subgroups for top-level types.  Distributions not
-        # belonging to these groups will be shown either in a "Others"
-        # group, or top-level if there are no other groups.
-        groups = {
-            "altlinux": _("ALT Linux"),
-            "centos": _("CentOS"),
-            "debian": _("Debian"),
-            "fedora": _("Fedora"),
-            "freebsd": _("FreeBSD"),
-            "mageia": _("Mageia"),
-            "netbsd": _("NetBSD"),
-            "openbsd": _("OpenBSD"),
-            "opensuse": _("openSUSE"),
-            "rhel": _("Red Hat Enterprise Linux"),
-            "sled": _("SUSE Linux Enterprise Desktop"),
-            "sles": _("SUSE Linux Enterprise Server"),
-            "ubuntu": _("Ubuntu"),
-        }
-
-        if self._show_all_os_was_selected:
-            # List all the OSes, and determine which OSes have groups,
-            # and which do not.
-            variants = virtinst.OSDB.list_os(typename=_type,
-                sortpref=preferred)
-            all_distros = set([_os.distro for _os in variants])
-            distros = [_os for _os in all_distros if _os in groups]
-            distros.sort()
-            other_distros = [_os for _os in all_distros if _os not in groups]
-            parents = dict()
-            if len(distros) > 0:
-                # We have groups for the OSes, so create them.
-                for d in distros:
-                    parents[d] = self._add_os_row(model, "", groups[d])
-                # Create the "Others" group at the end, for the OSes
-                # without a group.
-                if len(other_distros):
-                    others_parent = self._add_os_row(model, "", _('Others'))
-                    for d in other_distros:
-                        parents[d] = others_parent
-            else:
-                # No groups, so assume the top-level will be the parent
-                # all the OSes.
-                for d in other_distros:
-                    parents[d] = None
-            for v in variants:
-                self._add_os_row(model, v.name, v.label,
-                    parent=parents[v.distro])
-                self._add_completion_row(completion_model, v.name, v.label)
-        else:
-            # We are showing only the supported systems, so query them,
-            # and add them directly to their type.
-            variants = virtinst.OSDB.list_os(typename=_type,
-                sortpref=preferred, only_supported=True)
-            for v in variants:
-                self._add_os_row(model, v.name, v.label)
-                self._add_completion_row(completion_model, v.name, v.label)
-
-            # Add the menu entries to show all the OSes
-            self._add_os_row(model, sep=True)
-            self._add_os_row(model, label=_("Show all OS options"), action=True)
-
-        widget.set_active(0)
-
-    def _set_distro_labels(self, distro, ver):
-        self.widget("install-os-type-label").set_text(distro)
-        self.widget("install-os-version-label").set_text(ver)
-
-    def _set_os_id_in_ui(self, os_widget, os_id):
-        """
-        Helper method to set the os type/version widgets to the passed
-        OS ID value
-        """
-        model = os_widget.get_model()
-        def find_row():
-            def cmp_func(model, path, it, user_data):
-                ignore = path
-                if model.get_value(it, OS_COL_ID) == os_id:
-                    os_widget.set_active_iter(it)
-                    user_data[0] = model.get_value(it, OS_COL_LABEL)
-                    return True
-                return False
-            data = [None]
-            model.foreach(cmp_func, data)
-            label = data[0]
-            if not label:
-                os_widget.set_active(0)
-            return label
-
-        label = None
-        if os_id:
-            label = find_row()
-
-            if not label and not self._show_all_os_was_selected:
-                # We didn't find the OS in the variant UI, but we are only
-                # showing the reduced OS list. Trigger the _show_all_os option,
-                # and try again.
-                os_widget.set_active(len(model) - 1)
-                label = find_row()
-        return label or _("Unknown")
-
     def _set_distro_selection(self, variant):
         """
         Update the UI with the distro that was detected from the detection
@@ -1157,18 +948,16 @@ class vmmCreate(vmmGObjectUI):
             # update the UI
             return
 
-        distro_type = None
-        distro_var = None
+        name = self.widget("install-os-name")
         if variant:
-            osclass = virtinst.OSDB.lookup_os(variant)
-            distro_type = osclass.get_typename()
-            distro_var = osclass.name
+            self._os = virtinst.OSDB.lookup_os(variant)
+        else:
+            self._os = None
 
-        dl = self._set_os_id_in_ui(
-            self.widget("install-os-type"), distro_type)
-        vl = self._set_os_id_in_ui(
-            self.widget("install-os-version"), distro_var)
-        self._set_distro_labels(dl, vl)
+        if self._os is None:
+            name.set_text(_("None detected"))
+        else:
+            name.set_text(self._os.label)
 
 
     ###############################
@@ -1200,7 +989,6 @@ class vmmCreate(vmmGObjectUI):
         self.widget("summary-storage-path").set_markup(storagepath)
 
     def _populate_summary(self):
-        distro, version, ignore1, dlabel, vlabel = self._get_config_os_info()
         mem = _pretty_memory(int(self._guest.memory))
         cpu = str(int(self._guest.vcpus))
 
@@ -1221,21 +1009,7 @@ class vmmCreate(vmmGObjectUI):
         elif instmethod == INSTALL_PAGE_VZ_TEMPLATE:
             install = _("Virtuozzo container")
 
-        osstr = ""
-        have_os = True
-        if self._guest.os.is_container():
-            osstr = _("Linux")
-        elif not distro:
-            osstr = _("Generic")
-            have_os = False
-        elif not version:
-            osstr = _("Generic") + " " + dlabel
-            have_os = False
-        else:
-            osstr = vlabel
-
-        self.widget("finish-warn-os").set_visible(not have_os)
-        self.widget("summary-os").set_text(osstr)
+        self.widget("summary-os").set_text(self._os and self._os.label or _("Unknown"))
         self.widget("summary-install").set_text(install)
         self.widget("summary-mem").set_text(mem)
         self.widget("summary-cpu").set_text(cpu)
@@ -1317,31 +1091,6 @@ class vmmCreate(vmmGObjectUI):
                                                    INSTALL_PAGE_CONTAINER_OS,
                                                    INSTALL_PAGE_VZ_TEMPLATE]
 
-    def _get_config_os_info(self):
-        drow = uiutil.get_list_selected_row(self.widget("install-os-type"))
-        distro = None
-        dlabel = None
-        variant = None
-        entry = self.widget("install-os-version-entry")
-        vlabel = entry.get_text()
-
-        for row in entry.get_completion().get_model():
-            if row[OS_COL_LABEL] == vlabel:
-                variant = row[OS_COL_ID]
-                break
-
-        if not variant:
-            return (None, None, False, None, None)
-
-        if drow:
-            distro = drow[OS_COL_ID]
-            dlabel = drow[OS_COL_LABEL]
-
-        return (distro and str(distro),
-                str(variant),
-                True,
-                str(dlabel), str(vlabel))
-
     def _get_config_local_media(self, store_media=False):
         if self.widget("install-cdrom-radio").get_active():
             return self._mediacombo.get_path()
@@ -1506,56 +1255,30 @@ class vmmCreate(vmmGObjectUI):
     def _toggle_detect_os(self, src):
         dodetect = src.get_active()
 
-        self.widget("install-os-type-label").set_visible(dodetect)
-        self.widget("install-os-version-label").set_visible(dodetect)
-        self.widget("install-os-type").set_visible(not dodetect)
-        self.widget("install-os-version").set_visible(not dodetect)
+        self.widget("install-os-name").set_sensitive(not dodetect)
+        self.widget("install-os-name").set_text("")
+        self._os = None
 
         if dodetect:
-            self.widget("install-os-version-entry").set_text("")
             self._os_already_detected_for_media = False
             self._start_detect_os_if_needed()
 
-    def _selected_os_row(self):
-        return uiutil.get_list_selected_row(self.widget("install-os-type"))
-
-    def _change_os_type(self, box):
-        ignore = box
-        row = self._selected_os_row()
-        if not row:
-            return
-
-        _type = row[OS_COL_ID]
-        self._populate_os_variant_model(_type)
-        if not row[OS_COL_IS_SHOW_ALL]:
-            return
-
-        self._show_all_os_was_selected = True
-        self._populate_os_type_model()
-
-    def _change_os_version(self, box):
-        show_all = uiutil.get_list_selection(box,
-            column=OS_COL_IS_SHOW_ALL, check_entry=False)
-        if not show_all:
-            return
-
-        # 'show all OS' was clicked
-        # Get previous type to reselect it later
-        type_row = self._selected_os_row()
-        if not type_row:
-            return
-        old_type = type_row[OS_COL_ID]
-
-        self._show_all_os_was_selected = True
-        self._populate_os_type_model()
+    def _os_name_search_changed(self, src):
+        searchname = src.get_text().strip()
+        if self._os is None:
+            if src.get_sensitive() and searchname != "":
+                self._os_list.filter_name(searchname)
+                self._os_list.show(src)
+            else:
+                self._os_list.hide()
+        else:
+            if self._os.label != searchname:
+                self._os = None
+            self._os_list.hide()
 
-        # Reselect previous type row
-        os_type_list = self.widget("install-os-type")
-        os_type_model = os_type_list.get_model()
-        for idx, row in enumerate(os_type_model):
-            if row[OS_COL_ID] == old_type:
-                os_type_list.set_active(idx)
-                break
+    def _os_name_stop_search(self, src):
+        src.set_text("")
+        self._os_list.hide()
 
     def _local_media_toggled(self, src):
         usecdrom = src.get_active()
@@ -1568,19 +1291,6 @@ class vmmCreate(vmmGObjectUI):
         else:
             self._iso_changed(self.widget("install-iso-entry"))
 
-    def _os_detect_visibility_changed(self, src, ignore=None):
-        is_visible = src.get_visible()
-        detect_chkbox = self.widget("install-detect-os")
-        nodetect_label = self.widget("install-nodetect-label")
-
-        detect_chkbox.set_active(is_visible)
-        detect_chkbox.toggled()
-
-        if is_visible:
-            nodetect_label.hide()
-        else:
-            nodetect_label.show()
-
     def _browse_oscontainer(self, ignore):
         self._browse_file("install-oscontainer-fs", is_dir=True)
     def _browse_app(self, ignore):
@@ -1666,7 +1376,7 @@ class vmmCreate(vmmGObjectUI):
                 fs_dir = [os.environ['HOME'],
                           '.local/share/libvirt/filesystems/']
 
-            fs = fs_dir + [self._generate_default_name(None, None)]
+            fs = fs_dir + [self._generate_default_name(None)]
             self.widget("install-oscontainer-fs").set_text(os.path.join(*fs))
 
 
@@ -1737,7 +1447,9 @@ class vmmCreate(vmmGObjectUI):
     def _set_install_page(self):
         instnotebook = self.widget("install-method-pages")
         detectbox = self.widget("install-detect-os-box")
+        detect = self.widget("install-detect-os")
         osbox = self.widget("install-os-distro-box")
+        name = self.widget("install-os-name")
         instpage = self._get_config_install_page()
 
         # Setting OS value for a container guest doesn't really matter
@@ -1752,6 +1464,13 @@ class vmmCreate(vmmGObjectUI):
                         self._get_config_install_page() == INSTALL_PAGE_URL)
 
         detectbox.set_visible(enabledetect)
+        autodetect = detectbox.get_visible() and detect.get_active()
+        name.set_sensitive(not autodetect)
+        if enabledetect:
+            self._os = None
+        else:
+            if self._os is None:
+                name.set_text("")
 
         if instpage == INSTALL_PAGE_PXE:
             # Hide the install notebook for pxe, since there isn't anything
@@ -1915,18 +1634,16 @@ class vmmCreate(vmmGObjectUI):
             return False
         return True
 
-    def _generate_default_name(self, distro, variant):
+    def _generate_default_name(self, osobj):
         force_num = False
         if self._guest.os.is_container():
             basename = "container"
             force_num = True
-        elif not distro:
+        elif not osobj or not osobj.distro:
             basename = "vm"
             force_num = True
-        elif not variant:
-            basename = distro
         else:
-            basename = variant
+            basename = osobj.distro
 
         if self._guest.os.arch != self.conn.caps.host.cpu.arch:
             basename += "-%s" % _pretty_arch(self._guest.os.arch)
@@ -1949,9 +1666,8 @@ class vmmCreate(vmmGObjectUI):
         init = None
         fs = None
         template = None
-        distro, variant, valid, ignore1, ignore2 = self._get_config_os_info()
 
-        if not valid:
+        if self._os is None:
             return self.err.val_err(_("Please specify a valid OS variant."))
 
         if instmethod == INSTALL_PAGE_ISO:
@@ -2050,7 +1766,7 @@ class vmmCreate(vmmGObjectUI):
         try:
             # Overwrite the guest
             installer = instclass(self.conn.get_backend())
-            self._guest = self._build_guest(variant or distro)
+            self._guest = self._build_guest(self._os.name)
             if not self._guest:
                 return False
             self._guest.installer = installer
@@ -2114,7 +1830,7 @@ class vmmCreate(vmmGObjectUI):
                     self._capsinfo.arch)
 
         try:
-            name = self._generate_default_name(distro, variant)
+            name = self._generate_default_name(self._os)
             self.widget("create-vm-name").set_text(name)
             self._guest.name = name
         except Exception as e:
@@ -2138,11 +1854,10 @@ class vmmCreate(vmmGObjectUI):
                 self, self.conn, path)
 
         res = None
-        osobj = virtinst.OSDB.lookup_os(variant)
-        if osobj:
-            res = osobj.get_recommended_resources(self._guest)
-            logging.debug("Recommended resources for variant=%s: %s",
-                variant, res)
+        if self._os is not None:
+            res = self._os.get_recommended_resources(self._guest)
+            logging.debug("Recommended resources for os=%s: %s",
+                self._os.label, res)
 
         # Change the default values suggested to the user.
         ram_size = DEFAULT_MEM
@@ -2313,6 +2028,9 @@ class vmmCreate(vmmGObjectUI):
         if check_install_page and not is_install_page:
             return
         if not media:
+            name = self.widget("install-os-name")
+            if not name.get_sensitive():
+                name.set_text(_("Waiting for install media / source"))
             return
         if not self._is_os_detect_active():
             return
@@ -2357,6 +2075,9 @@ class vmmCreate(vmmGObjectUI):
         detectThread.setDaemon(True)
         detectThread.start()
 
+        spin = self.widget("install-detect-os-spinner")
+        spin.start()
+
         self._report_detect_os_progress(0, thread_results,
                 forward_after_finish)
 
@@ -2385,15 +2106,10 @@ class vmmCreate(vmmGObjectUI):
         chance of the detection hanging (like slow URL lookup)
         """
         try:
-            base = _("Detecting")
-
             if (thread_results.in_progress() and
                 (idx < (DETECT_TIMEOUT * 2))):
                 # Thread is still going and we haven't hit the timeout yet,
                 # so update the UI labels and reschedule this function
-                detect_str = base + ("." * ((idx % 3) + 1))
-                self._set_distro_labels(detect_str, detect_str)
-
                 self.timeout_add(500, self._report_detect_os_progress,
                     idx + 1, thread_results, forward_after_finish)
                 return
@@ -2403,6 +2119,8 @@ class vmmCreate(vmmGObjectUI):
             distro = None
             logging.exception("Error in distro detect timeout")
 
+        spin = self.widget("install-detect-os-spinner")
+        spin.stop()
         logging.debug("Finished UI OS detection.")
 
         self.widget("create-forward").set_sensitive(True)
diff --git a/virtManager/oslist.py b/virtManager/oslist.py
new file mode 100644
index 00000000..fc60f346
--- /dev/null
+++ b/virtManager/oslist.py
@@ -0,0 +1,106 @@
+# Copyright (C) 2018 Red Hat, Inc.
+#
+# This work is licensed under the GNU GPLv2 or later.
+# See the COPYING file in the top-level directory.
+
+import logging
+
+from gi.repository import Gtk
+
+import virtinst
+
+from .baseclass import vmmGObjectUI
+
+
+class vmmOSList(vmmGObjectUI):
+    __gsignals__ = {
+        "os-selected": (vmmGObjectUI.RUN_FIRST, None, [object])
+    }
+
+    def __init__(self):
+        vmmGObjectUI.__init__(self, "oslist.ui", "vmm-oslist")
+        self._cleanup_on_app_close()
+
+        self._filter_name = None
+        self._filter_eol = True
+
+        self.builder.connect_signals({
+            "on_include_eol_toggled": self._eol_toggled,
+        })
+
+        self._init_state()
+
+    def _init_state(self):
+
+        self.topwin.set_modal(False)
+        os_list = self.widget("os-list")
+
+        # (os object, label)
+        os_list_model = Gtk.ListStore(object, str)
+
+        all_os = virtinst.OSDB.list_os()
+
+        for os in all_os:
+            os_list_model.append([os, "%s (%s)" % (os.label, os.name)])
+
+        self._os_list_model = Gtk.TreeModelFilter(child_model=os_list_model)
+        self._os_list_model.set_visible_func(self._filter_os)
+
+        os_list.set_model(self._os_list_model)
+
+        nameCol = Gtk.TreeViewColumn(_("Name"))
+        nameCol.set_spacing(6)
+
+        text = Gtk.CellRendererText()
+        nameCol.pack_start(text, True)
+        nameCol.add_attribute(text, 'text', 1)
+        os_list.append_column(nameCol)
+
+        os_list.connect("row_activated", self._os_selected)
+
+    def _eol_toggled(self, src):
+        self._filter_eol = not src.get_active()
+        self._refilter()
+
+    def _os_selected(self, tree_view, path, column):
+        model, titer = tree_view.get_selection().get_selected()
+        if titer is None:
+            self.emit("os-selected", None)
+        else:
+            self.emit("os-selected", model[titer][0])
+
+    def _filter_os(self, model, titer, ignore1):
+        os = model.get(titer, 0)[0]
+        if self._filter_eol:
+            if os.eol:
+                return False
+
+        if self._filter_name is not None and self._filter_name != "":
+            label = os.label.lower()
+            name = os.name.lower()
+            if (label.find(self._filter_name) == -1 and
+                name.find(self._filter_name) == -1):
+                return False
+
+        return True
+
+    def _refilter(self):
+        os_list = self.widget("os-list")
+        sel = os_list.get_selection()
+        sel.unselect_all()
+        self._os_list_model.refilter()
+
+    def filter_name(self, partial_name):
+        self._filter_name = partial_name.lower()
+        self._refilter()
+
+    def show(self, parent):
+        logging.debug("Showing oslist")
+        self.topwin.set_relative_to(parent)
+        self.topwin.popup()
+
+    def hide(self):
+        self.topwin.popdown()
+
+    def _cleanup(self):
+        pass
diff --git a/virtinst/osdict.py b/virtinst/osdict.py
index 425557c8..ebeefef2 100644
--- a/virtinst/osdict.py
+++ b/virtinst/osdict.py
@@ -10,62 +10,22 @@
 import datetime
 import logging
 import re
+import time
 
 import gi
 gi.require_version('Libosinfo', '1.0')
 from gi.repository import Libosinfo as libosinfo
+from gi.repository import GLib
 
 
 ###################
 # Sorting helpers #
 ###################
 
-def _remove_older_point_releases(distro_list):
-    ret = distro_list[:]
-
-    def _get_minor_version(osobj):
-        return int(osobj.name.rsplit(".", 1)[-1])
-
-    def _find_latest(prefix):
-        """
-        Given a prefix like 'rhel4', find the latest 'rhel4.X',
-        and remove the rest from the os list
-        """
-        latest_os = None
-        first_id = None
-        for osobj in ret[:]:
-            if not re.match("%s\.\d+" % prefix, osobj.name):
-                continue
-
-            if first_id is None:
-                first_id = ret.index(osobj)
-            ret.remove(osobj)
-
-            if (latest_os and
-                _get_minor_version(latest_os) > _get_minor_version(osobj)):
-                continue
-            latest_os = osobj
-
-        if latest_os:
-            ret.insert(first_id, latest_os)
-
-    _find_latest("rhel4")
-    _find_latest("rhel5")
-    _find_latest("rhel6")
-    _find_latest("rhel7")
-    _find_latest("freebsd9")
-    _find_latest("freebsd10")
-    _find_latest("freebsd11")
-    _find_latest("centos6")
-    _find_latest("centos7")
-    return ret
-
-
-def _sort(tosort, sortpref=None, limit_point_releases=False):
+def _sort(tosort):
     sortby_mappings = {}
     distro_mappings = {}
     retlist = []
-    sortpref = sortpref or []
 
     for key, osinfo in tosort.items():
         # Libosinfo has some duplicate version numbers here, so append .1
@@ -90,15 +50,8 @@ def _sort(tosort, sortpref=None, limit_point_releases=False):
         distro_list.sort()
         distro_list.reverse()
 
-    # Move the sortpref values to the front of the list
     sorted_distro_list = list(distro_mappings.keys())
     sorted_distro_list.sort()
-    sortpref.reverse()
-    for prefer in sortpref:
-        if prefer not in sorted_distro_list:
-            continue
-        sorted_distro_list.remove(prefer)
-        sorted_distro_list.insert(0, prefer)
 
     # Build the final list of sorted os objects
     for distro in sorted_distro_list:
@@ -107,10 +60,6 @@ def _sort(tosort, sortpref=None, limit_point_releases=False):
             orig_key = sortby_mappings[key]
             retlist.append(tosort[orig_key])
 
-    # Filter out older point releases
-    if limit_point_releases:
-        retlist = _remove_older_point_releases(retlist)
-
     return retlist
 
 
@@ -236,25 +185,16 @@ class _OSDB(object):
             "solaris", "other", "generic"]
         return approved_types
 
-    def list_os(self, typename=None, only_supported=False, sortpref=None):
+    def list_os(self):
         """
         List all OSes in the DB
-
-        :param typename: Only list OSes of this type
-        :param only_supported: Only list OSses where self.supported == True
-        :param sortpref: Sort these OSes at the front of the list
         """
         sortmap = {}
 
         for name, osobj in self._all_variants.items():
-            if typename and typename != osobj.get_typename():
-                continue
-            if only_supported and not osobj.get_supported():
-                continue
             sortmap[name] = osobj
 
-        return _sort(sortmap, sortpref=sortpref,
-            limit_point_releases=only_supported)
+        return _sort(sortmap)
 
     def latest_regex(self, regex):
         """
@@ -282,6 +222,24 @@ class _OsVariant(object):
         self.label = self._os and self._os.get_name() or "Generic"
         self.codename = self._os and self._os.get_codename() or ""
         self.distro = self._os and self._os.get_distro() or ""
+        self.eol = False
+
+        eol = self._os and self._os.get_eol_date() or None
+        rel = self._os and self._os.get_release_date() or None
+
+        # End of life if an EOL date is present and has past,
+        # or if the release date is present and was 5 years or more
+        if eol is not None:
+            now = GLib.Date()
+            now.set_time_t(time.time())
+            if eol.compare(now) < 0:
+                self.eol = True
+        elif rel is not None:
+            then = GLib.Date()
+            then.set_time_t(time.time())
+            then.subtract_years(5)
+            if rel.compare(then) < 0:
+                self.eol = True
 
         self.sortby = self._get_sortby()
         self.urldistro = self._get_urldistro()
-- 
2.14.3




More information about the virt-tools-list mailing list