[gnome-boxes/wip/rishi/rhel: 17/17] wizard, wizard-source: Support gratis RHEL boxes



commit f1794f3135d6b522987c7d167b283cf4c652b012
Author: Debarshi Ray <debarshir gnome org>
Date:   Thu Aug 24 18:19:50 2017 +0200

    wizard, wizard-source: Support gratis RHEL boxes
    
    Since March 2016, it is possible to obtain an unsupported copy of Red
    Hat Enterprise Linux (or RHEL) for gratis [1] that's covered by the
    RHEL Developer Suite subscription. This makes it easier to set up such
    VMs by teaching the wizard how to download the ISOs.
    
    [1] https://developers.redhat.com/blog/2016/03/31/no-cost-rhel-developer-subscription-now-available/
    
    https://bugzilla.gnome.org/show_bug.cgi?id=786679

 data/ui/wizard-source.ui |  105 ++++++++++++++
 src/wizard-source.vala   |  344 +++++++++++++++++++++++++++++++++++++++++++++-
 src/wizard.vala          |   35 ++++-
 3 files changed, 477 insertions(+), 7 deletions(-)
---
diff --git a/data/ui/wizard-source.ui b/data/ui/wizard-source.ui
index b16292a..b4054d9 100644
--- a/data/ui/wizard-source.ui
+++ b/data/ui/wizard-source.ui
@@ -119,6 +119,78 @@
         </child>
 
         <child>
+          <object class="GtkButton" id="install_rhel_button">
+            <property name="visible">True</property>
+            <signal name="clicked" handler="on_install_rhel_button_clicked"/>
+            <style>
+              <class name="boxes-menu-row"/>
+            </style>
+            <child>
+              <object class="GtkBox" id="install_rhel_hbox">
+                <property name="visible">True</property>
+                <property name="margin-start">15</property>
+                <property name="margin-end">15</property>
+                <property name="spacing">20</property>
+                <property name="orientation">horizontal</property>
+                <child>
+                  <object class="GtkImage" id="install_rhel_image">
+                    <property name="icon-size">0</property>
+                    <property name="no-show-all">True</property>
+                    <property name="pixel-size">64</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">False</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkBox" id="install_rhel_vbox">
+                    <property name="visible">True</property>
+                    <property name="homogeneous">True</property>
+                    <property name="orientation">vertical</property>
+                    <child>
+                      <object class="GtkLabel" id="install_rhel_label">
+                        <property name="visible">True</property>
+                        <property name="ellipsize">end</property>
+                        <property name="halign">start</property>
+                        <property name="valign">end</property>
+                        <property name="use-underline">True</property>
+                        <property name="label" translatable="yes">Red Hat Enterprise Linux</property>
+                      </object>
+                      <packing>
+                        <property name="expand">True</property>
+                        <property name="fill">True</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="install_rhel_details_label">
+                        <property name="visible">True</property>
+                        <property name="ellipsize">end</property>
+                        <property name="halign">start</property>
+                        <property name="valign">start</property>
+                        <property name="label" translatable="yes">Available with a free Red Hat developer 
account</property>
+                        <style>
+                          <class name="boxes-step-label"/>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="expand">True</property>
+                        <property name="fill">True</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">True</property>
+                    <property name="fill">True</property>
+                  </packing>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+
+        <child>
           <object class="GtkButton" id="enter_url_button">
             <property name="visible">True</property>
             <signal name="clicked" handler="on_enter_url_button_clicked"/>
@@ -227,6 +299,39 @@
       </packing>
     </child>
 
+    <!-- RHEL web view page -->
+    <child>
+      <object class="GtkStack" id="rhel_web_view_stack">
+        <property name="visible">True</property>
+        <child>
+          <object class="GtkSpinner" id="rhel_web_view_spinner">
+            <property name="halign">center</property>
+            <property name="valign">center</property>
+            <property name="visible">True</property>
+            <property name="width-request">32</property>
+            <property name="height-request">32</property>
+          </object>
+          <packing>
+            <property name="name">spinner</property>
+          </packing>
+        </child>
+        <child>
+          <object class="BoxesWizardWebView" id="rhel_web_view">
+            <property name="hexpand">True</property>
+            <property name="vexpand">True</property>
+            <property name="visible">True</property>
+          </object>
+          <packing>
+            <property name="name">web-view</property>
+          </packing>
+        </child>
+      </object>
+
+      <packing>
+        <property name="name">rhel-web-view-page</property>
+      </packing>
+    </child>
+
     <!-- URL page -->
     <child>
       <object class="GtkBox" id="url_menubox">
diff --git a/src/wizard-source.vala b/src/wizard-source.vala
index 27afcf9..28116e4 100644
--- a/src/wizard-source.vala
+++ b/src/wizard-source.vala
@@ -2,6 +2,7 @@
 
 private enum Boxes.SourcePage {
     MAIN,
+    RHEL_WEB_VIEW,
     URL,
 
     LAST,
@@ -202,7 +203,7 @@ private class Boxes.WizardWebView : Gtk.Bin {
 
 [GtkTemplate (ui = "/org/gnome/Boxes/ui/wizard-source.ui")]
 private class Boxes.WizardSource: Gtk.Stack {
-    private const string[] page_names = { "main-page", "url-page" };
+    private const string[] page_names = { "main-page", "rhel-web-view-page", "url-page" };
 
     public Gtk.Widget? selected { get; set; }
     public string uri {
@@ -229,6 +230,16 @@ private class Boxes.WizardSource: Gtk.Stack {
     private Gtk.Button libvirt_sys_import_button;
     [GtkChild]
     private Gtk.Label libvirt_sys_import_label;
+    [GtkChild]
+    private Gtk.Button install_rhel_button;
+    [GtkChild]
+    private Gtk.Image install_rhel_image;
+    [GtkChild]
+    private Boxes.WizardWebView rhel_web_view;
+    [GtkChild]
+    private Gtk.Spinner rhel_web_view_spinner;
+    [GtkChild]
+    private Gtk.Stack rhel_web_view_stack;
 
     private AppWindow window;
 
@@ -236,8 +247,14 @@ private class Boxes.WizardSource: Gtk.Stack {
 
     private Gtk.ListStore? media_urls_store;
 
+    private Cancellable? rhel_cancellable;
+    private Gtk.TreeRowReference? rhel_os_row_reference;
+    private Osinfo.Os? rhel_os;
+
     public MediaManager media_manager;
 
+    public string filename { get; set; }
+
     public bool download_required {
         get {
             const string[] supported_schemes = { "http", "https" };
@@ -267,6 +284,12 @@ private class Boxes.WizardSource: Gtk.Stack {
         set {
             _page = value;
 
+            rhel_web_view_spinner.stop ();
+            if (rhel_cancellable != null) {
+                rhel_cancellable.cancel ();
+                rhel_cancellable = null;
+            }
+
             visible_child_name = page_names[value];
 
             if (selected != null)
@@ -277,6 +300,9 @@ private class Boxes.WizardSource: Gtk.Stack {
                 // FIXME: grab first element in the menu list
                 main_vbox.grab_focus ();
                 break;
+            case SourcePage.RHEL_WEB_VIEW:
+                rhel_web_view_stack.set_visible_child_name ("spinner");
+                break;
             case SourcePage.URL:
                 url_entry.changed ();
                 url_entry.grab_focus ();
@@ -297,6 +323,17 @@ private class Boxes.WizardSource: Gtk.Stack {
         update_libvirt_sytem_entry_visibility.begin ();
         add_media_entries.begin ();
         transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT; // FIXME: Why this won't work from .ui 
file?
+
+        rhel_web_view.view.decide_policy.connect (on_rhel_web_view_decide_policy);
+    }
+
+    public override void dispose () {
+        if (rhel_cancellable != null) {
+            rhel_cancellable.cancel ();
+            rhel_cancellable = null;
+        }
+
+        base.dispose ();
     }
 
     public void setup_ui (AppWindow window) {
@@ -305,6 +342,7 @@ private class Boxes.WizardSource: Gtk.Stack {
         this.window = window;
 
         var os_db = media_manager.os_db;
+
         os_db.get_all_media_urls_as_store.begin ((db, result) => {
             try {
                 media_urls_store = os_db.get_all_media_urls_as_store.end (result);
@@ -325,14 +363,48 @@ private class Boxes.WizardSource: Gtk.Stack {
                 debug ("Failed to get all known media URLs: %s", error.message);
             }
         });
+
+        // We need a Shadowman logo and libosinfo mandates that we specify an
+        // OsinfoOs to get a logo. However, we don't have an OsinfoOs to begin
+        // with, and by the time we get one from the Red Hat developer portal
+        // it will be too late.
+        //
+        // To work around this, we specify the ID of a RHEL release and use it
+        // to get an OsinfoOs. Since all RHEL releases have the same Shadowman,
+        // the exact version of the RHEL release doesn't matter.
+        //
+        // Ideally, distributions would be a first-class object in libosinfo, so
+        // that we could query for RHEL instead of a specific version of it.
+        var rhel_id = "http://redhat.com/rhel/7.4";;
+
+        os_db.get_os_by_id.begin (rhel_id, (obj, res) => {
+            try {
+                rhel_os = os_db.get_os_by_id.end (res);
+            } catch (OSDatabaseError error) {
+                warning ("Failed to find OS with ID '%s': %s", rhel_id, error.message);
+                return;
+            }
+
+            Downloader.fetch_os_logo.begin (install_rhel_image, rhel_os, 64, (obj, res) => {
+                Downloader.fetch_os_logo.end (res);
+                var pixbuf = install_rhel_image.pixbuf;
+                install_rhel_image.visible = pixbuf != null;
+            });
+        });
     }
 
     public void cleanup () {
+        filename = null;
         install_media = null;
         libvirt_sys_import = false;
         selected = null;
         if(page != SourcePage.URL)
             uri = "";
+
+        if (rhel_cancellable != null) {
+            rhel_cancellable.cancel ();
+            rhel_cancellable = null;
+        }
     }
 
     [GtkCallback]
@@ -443,4 +515,274 @@ private class Boxes.WizardSource: Gtk.Stack {
             warning ("Failed to setup installation media '%s': %s", media.device_file, error.message);
         }
     }
+
+    private string rhel_get_authentication_uri_from_json (string contents) throws GLib.Error
+        requires (contents.length > 0) {
+
+        var parser = new Json.Parser ();
+        parser.load_from_data (contents, -1);
+
+        Json.NodeType node_type = Json.NodeType.NULL;
+
+        var root_node = parser.get_root ();
+        node_type = root_node.get_node_type ();
+        if (node_type != Json.NodeType.ARRAY)
+            throw new Boxes.Error.INVALID ("Failed to parse JSON: couldn’t find root array");
+
+        var root_array = root_node.get_array ();
+        if (root_array == null)
+            throw new Boxes.Error.INVALID ("Failed to parse JSON: couldn’t find root array");
+        if (root_array.get_length () == 0)
+            throw new Boxes.Error.INVALID ("Failed to parse JSON: root array is empty");
+
+        var root_array_node_0 = root_array.get_element (0);
+        node_type = root_array_node_0.get_node_type ();
+        if (node_type != Json.NodeType.OBJECT)
+            throw new Boxes.Error.INVALID ("Failed to parse JSON: root array doesn’t have an object");
+
+        var root_array_object_0 = root_array_node_0.get_object ();
+
+        var product_code_node = root_array_object_0.get_member ("productCode");
+        if (product_code_node == null)
+            throw new Boxes.Error.INVALID ("Failed to parse JSON: couldn’t find productCode");
+        node_type = product_code_node.get_node_type ();
+        if (node_type != Json.NodeType.VALUE)
+            throw new Boxes.Error.INVALID ("Failed to parse JSON: productCode is not a VALUE");
+
+        var product_code = product_code_node.get_string ();
+        if (product_code != "rhel")
+            throw new Boxes.Error.INVALID ("Failed to parse JSON: productCode is not rhel");
+
+        var featured_artifact_node = root_array_object_0.get_member ("featuredArtifact");
+        if (featured_artifact_node == null)
+            throw new Boxes.Error.INVALID ("Failed to parse JSON: couldn’t find featuredArtifact");
+        node_type = featured_artifact_node.get_node_type ();
+        if (node_type != Json.NodeType.OBJECT)
+            throw new Boxes.Error.INVALID ("Failed to parse JSON: featuredArtifact is not an OBJECT");
+
+        var featured_artifact_object = featured_artifact_node.get_object ();
+
+        var url_node = featured_artifact_object.get_member ("url");
+        if (url_node == null)
+            throw new Boxes.Error.INVALID ("Failed to parse JSON: couldn’t find featuredArtifact.url");
+        node_type = url_node.get_node_type ();
+        if (node_type != Json.NodeType.VALUE)
+            throw new Boxes.Error.INVALID ("Failed to parse JSON: featuredArtifact.url is not a VALUE");
+
+        var url = url_node.get_string ();
+        if (url == null || url.length == 0)
+            throw new Boxes.Error.INVALID ("Failed to parse JSON: featuredArtifact.url is empty");
+
+        return url;
+    }
+
+    private string rhel_get_authentication_uri_from_xml (string contents) throws GLib.Error
+        requires (contents.length > 0) {
+
+        var product_code = extract_xpath (contents, "string(/products/product/productCode)", true);
+        if (product_code != "rhel")
+            throw new Boxes.Error.INVALID ("Failed to parse XML: productCode is not rhel");
+
+        var url = extract_xpath (contents, "string(/products/product/featuredArtifact/url)", true);
+        if (url.length == 0)
+            throw new Boxes.Error.INVALID ("Failed to parse XML: featuredArtifact.url is empty");
+
+        return url;
+    }
+
+    private void rhel_show_web_view (string cached_path, bool use_cache) {
+        if (!use_cache) {
+            GLib.FileUtils.unlink (cached_path);
+            rhel_web_view_spinner.start ();
+        }
+
+        var downloader = Downloader.get_instance ();
+        var file = GLib.File.new_for_uri 
("https://developers.redhat.com/download-manager/rest/available/rhel";);
+        string[] cached_paths = { cached_path };
+        var progress = new ActivityProgress ();
+        downloader.download.begin (file, cached_paths, progress, rhel_cancellable, (obj, res) => {
+            try {
+                file = downloader.download.end (res);
+            } catch (GLib.IOError.CANCELLED error) {
+                return;
+            } catch (GLib.Error error) {
+                page = SourcePage.MAIN;
+                window.notificationbar.display_error (_("Failed to get authentication URI"));
+                warning (error.message);
+                return;
+            }
+
+            file.load_contents_async.begin (rhel_cancellable, (obj, res) => {
+                uint8[] contents;
+                try {
+                    file.load_contents_async.end (res, out contents, null);
+                } catch (GLib.IOError.CANCELLED error) {
+                    return;
+                } catch (GLib.Error error) {
+                    page = SourcePage.MAIN;
+                    window.notificationbar.display_error (_("Failed to parse response from redhat.com"));
+                    warning (error.message);
+                    return;
+                }
+
+                if (contents.length <= 0) {
+                    page = SourcePage.MAIN;
+                    window.notificationbar.display_error (_("Failed to parse response from redhat.com"));
+                    warning ("Empty response from redhat.com");
+                    return;
+                }
+
+                string? authentication_uri;
+                try {
+                    authentication_uri = rhel_get_authentication_uri_from_json ((string) contents);
+                } catch (Boxes.Error error) {
+                    page = SourcePage.MAIN;
+                    window.notificationbar.display_error (_("Failed to parse response from redhat.com"));
+                    warning (error.message);
+                    return;
+                } catch (GLib.Error json_error) {
+                    debug ("Failed to parse as JSON, could be XML: %s", json_error.message);
+                    try {
+                        authentication_uri = rhel_get_authentication_uri_from_xml ((string) contents);
+                    } catch (GLib.Error xml_error) {
+                        page = SourcePage.MAIN;
+                        window.notificationbar.display_error (_("Failed to parse response from redhat.com"));
+                        warning (xml_error.message);
+                        return;
+                    }
+                }
+
+                debug ("RHEL ISO authentication URI: %s", authentication_uri);
+
+                rhel_cancellable = new GLib.Cancellable ();
+                rhel_cancellable.connect(() => {
+                    rhel_web_view.view.stop_loading ();
+                    rhel_web_view.view.load_uri ("about:blank");
+
+                    var data_manager = rhel_web_view.view.get_website_data_manager ();
+                    data_manager.clear.begin (WebKit.WebsiteDataTypes.COOKIES, 0, null);
+                });
+
+                if (rhel_web_view_spinner.active) {
+                    rhel_web_view_stack.set_visible_child_full ("web-view", 
Gtk.StackTransitionType.CROSSFADE);
+                    rhel_web_view_spinner.stop ();
+                } else {
+                    rhel_web_view_stack.set_visible_child_name ("web-view");
+                }
+
+                rhel_web_view.view.load_uri (authentication_uri);
+                filename = GLib.Path.get_basename (authentication_uri);
+            });
+        });
+    }
+
+    [GtkCallback]
+    private void on_install_rhel_button_clicked () {
+        page = SourcePage.RHEL_WEB_VIEW;
+        rhel_cancellable = new GLib.Cancellable ();
+
+        var cached_path = get_cache ("developers.redhat.com", "rhel");
+        var cached_file = GLib.File.new_for_path (cached_path);
+        cached_file.query_info_async.begin (GLib.FileAttribute.TIME_MODIFIED,
+                                            GLib.FileQueryInfoFlags.NONE,
+                                            GLib.Priority.DEFAULT,
+                                            rhel_cancellable,
+                                            (obj, res) => {
+            GLib.FileInfo? info;
+            try {
+                info = cached_file.query_info_async.end (res);
+            } catch (GLib.IOError.CANCELLED error) {
+                return;
+            } catch (GLib.IOError.NOT_FOUND error) {
+                debug ("No cached response from redhat.com");
+                rhel_show_web_view (cached_path, false);
+                return;
+            } catch (GLib.Error error) {
+                warning ("Failed to find cached response from redhat.com: %s", error.message);
+                rhel_show_web_view (cached_path, false);
+                return;
+            }
+
+            var mtime_timeval = info.get_modification_time ();
+            GLib.DateTime? mtime = new GLib.DateTime.from_timeval_utc (mtime_timeval);
+            if (mtime == null) {
+                warning ("Cached response from redhat.com has invalid modification time");
+                rhel_show_web_view (cached_path, false);
+                return;
+            }
+
+            GLib.DateTime? now = new GLib.DateTime.now_utc ();
+            if (now == null) {
+                warning ("Failed to read current time");
+                rhel_show_web_view (cached_path, false);
+                return;
+            }
+
+            var time_difference = now.difference (mtime);
+            if (time_difference > GLib.TimeSpan.DAY) {
+                debug ("Cached response from redhat.com is more than a day old");
+                rhel_show_web_view (cached_path, false);
+                return;
+            }
+
+            debug ("Cached response from redhat.com is less than a day old");
+            rhel_show_web_view (cached_path, true);
+        });
+    }
+
+    private bool on_rhel_web_view_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";))
+            return false;
+
+        var soup_uri = new Soup.URI (request_uri);
+        var query = soup_uri.get_query ();
+        if (query == null)
+            return false;
+
+        var key_value_pairs = Soup.Form.decode (query);
+        var download_uri = key_value_pairs.lookup ("tcDownloadURL");
+        if (download_uri == null)
+            return false;
+
+        debug ("RHEL ISO download URI: %s", download_uri);
+
+        if (rhel_os != null) {
+            Gtk.TreeIter iter;
+            Gtk.TreePath? path;
+            bool iter_is_valid = false;
+
+            if (rhel_os_row_reference == null) {
+                media_urls_store.append (out iter);
+                iter_is_valid = true;
+
+                path = media_urls_store.get_path (iter);
+                rhel_os_row_reference = new Gtk.TreeRowReference (media_urls_store, path);
+            } else {
+                path = rhel_os_row_reference.get_path ();
+                iter_is_valid = media_urls_store.get_iter (out iter, path);
+            }
+
+            if (iter_is_valid) {
+                media_urls_store.set (iter,
+                                      OSDatabase.MediaURLsColumns.URL, download_uri,
+                                      OSDatabase.MediaURLsColumns.OS, rhel_os);
+            }
+        }
+
+        uri = download_uri;
+        activated ();
+
+        selected = install_rhel_button;
+
+        decision.ignore ();
+        return true;
+    }
 }
diff --git a/src/wizard.vala b/src/wizard.vala
index 5c051af..78a9c14 100644
--- a/src/wizard.vala
+++ b/src/wizard.vala
@@ -119,6 +119,13 @@ private class Boxes.Wizard: Gtk.Stack, Boxes.UI {
                     return;
                 }
             } else {
+                switch (value) {
+                case WizardPage.SOURCE:
+                    if (wizard_source.page == SourcePage.RHEL_WEB_VIEW)
+                        wizard_source.page = SourcePage.MAIN;
+                    break;
+                }
+
                 switch (page) {
                 case WizardPage.REVIEW:
                     create_button.visible = false;
@@ -133,7 +140,7 @@ private class Boxes.Wizard: Gtk.Stack, Boxes.UI {
                 return;
 
             _page = value;
-            back_button.sensitive = (value != WizardPage.SOURCE);
+            update_back_button_sensitivity ();
             wizard_window.topbar.set_title_for_page (value);
             visible_child_name = page_names[value];
 
@@ -154,6 +161,7 @@ private class Boxes.Wizard: Gtk.Stack, Boxes.UI {
     }
 
     private void wizard_source_update_buttons () {
+        update_back_button_sensitivity ();
         if (page != WizardPage.SOURCE)
             return;
 
@@ -165,6 +173,10 @@ private class Boxes.Wizard: Gtk.Stack, Boxes.UI {
             source = null;
             break;
 
+        case Boxes.SourcePage.RHEL_WEB_VIEW:
+            next_button.sensitive = false;
+            break;
+
         case Boxes.SourcePage.URL:
             next_button.sensitive = false;
             if (wizard_source.uri.length == 0)
@@ -186,6 +198,12 @@ private class Boxes.Wizard: Gtk.Stack, Boxes.UI {
         }
     }
 
+    private void update_back_button_sensitivity () {
+        var disable_back_button = page == WizardPage.SOURCE &&
+                                  (wizard_source.page == SourcePage.MAIN || wizard_source.page == 
SourcePage.URL);
+        back_button.sensitive = !disable_back_button;
+    }
+
     construct {
         media_manager = MediaManager.get_instance ();
         wizard_source.notify["page"].connect(wizard_source_update_buttons);
@@ -397,7 +415,7 @@ private class Boxes.Wizard: Gtk.Stack, Boxes.UI {
 
         try {
             // Validate URI
-            prepare_for_location (wizard_source.uri, null, true);
+            prepare_for_location (wizard_source.uri, wizard_source.filename, true);
         } catch (GLib.Error error) {
             window.notificationbar.display_error (error.message);
 
@@ -408,7 +426,7 @@ private class Boxes.Wizard: Gtk.Stack, Boxes.UI {
 
         if (wizard_source.download_required) {
             continue_button.sensitive = false;
-            download_media.begin (wizard_source.uri, null, progress);
+            download_media.begin (wizard_source.uri, wizard_source.filename, progress);
 
             var os = wizard_source.get_os_from_uri (wizard_source.uri);
             if (os == null)
@@ -641,9 +659,14 @@ private class Boxes.Wizard: Gtk.Stack, Boxes.UI {
         cancel_button.clicked.connect (cancel);
         back_button = wizard_window.topbar.back_btn;
         back_button.clicked.connect (() => {
-            prepare_cancellable.cancel ();
-
-            page = page - 1;
+            if (page == WizardPage.SOURCE) {
+                return_if_fail (wizard_source.page == SourcePage.RHEL_WEB_VIEW);
+                wizard_source.page = SourcePage.MAIN;
+                wizard_source.cleanup ();
+            } else {
+                prepare_cancellable.cancel ();
+                page = page - 1;
+            }
         });
         continue_button = wizard_window.topbar.continue_btn;
         continue_button.clicked.connect (() => {


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