diff --git a/src/Dialogs/DecryptDialog.vala b/src/Dialogs/DecryptDialog.vala index ea69a945c..1bcb79b45 100644 --- a/src/Dialogs/DecryptDialog.vala +++ b/src/Dialogs/DecryptDialog.vala @@ -263,6 +263,7 @@ public class DecryptDialog: Gtk.Dialog { unowned Distinst.Disks disks = options.borrow_disks (); foreach (unowned Distinst.Partition partition in disks.get_encrypted_partitions ()) { string path = Utils.string_from_utf8 (partition.get_device_path ()); + bool is_unlocked = options.is_unlocked (path); var lock_icon_name = is_unlocked ? "emblem-unlocked" : "dialog-password"; diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 9bd5a487f..c59fdba77 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -21,6 +21,7 @@ public class Installer.MainWindow : Gtk.Dialog { private Gtk.Stack stack; + private AlongsideView alongside_view; private DiskView disk_view; private EncryptView encrypt_view; private ErrorView error_view; @@ -30,6 +31,7 @@ public class Installer.MainWindow : Gtk.Dialog { private PartitioningView partitioning_view; private ProgressView progress_view; private RefreshView refresh_view; + private ResizeView resize_view; private SuccessView success_view; private TryInstallView try_install_view; private bool check_ignored = false; @@ -132,6 +134,45 @@ public class Installer.MainWindow : Gtk.Dialog { try_install_view.custom_step.connect (load_partitioning_view); try_install_view.next_step.connect (load_disk_view); try_install_view.refresh_step.connect (load_refresh_view); + try_install_view.alongside_step.connect (load_alongside_view); + } + + private void load_alongside_view () { + if (alongside_view == null) { + alongside_view = new AlongsideView (); + alongside_view.previous_view = try_install_view; + alongside_view.next_step.connect ((set_scale, os, free, total) => { + if (set_scale) { + load_resize_view (os, free, total); + } else { + load_encrypt_view (); + } + }); + + alongside_view.cancel.connect (() => { + stack.visible_child = try_install_view; + }); + stack.add (alongside_view); + } + + stack.visible_child = alongside_view; + alongside_view.update_options (); + } + + private void load_resize_view (string? os, uint64 free, uint64 total) { + if (resize_view == null) { + resize_view = new ResizeView (minimum_disk_size); + resize_view.previous_view = alongside_view; + resize_view.next_step.connect (load_encrypt_view); + resize_view.cancel.connect (() => { + stack.visible_child = alongside_view; + }); + + stack.add (resize_view); + } + + stack.visible_child = resize_view; + resize_view.update_options (os, free, total); } private void load_refresh_view () { diff --git a/src/Objects/InstallOptions.vala b/src/Objects/InstallOptions.vala index 36a90e6a1..f3b794b85 100644 --- a/src/Objects/InstallOptions.vala +++ b/src/Objects/InstallOptions.vala @@ -29,6 +29,9 @@ public class InstallOptions : GLib.Object { private Gee.ArrayList unlocked_devices { get; set; default = new Gee.ArrayList (); } + private unowned Distinst.Disk? install_device; + private string? install_device_path; + // The amount of free space that should be retained when shrinking (20 GiB). public const uint64 SHRINK_OVERHEAD = 20 * 2 * 1024 * 1024; @@ -40,6 +43,25 @@ public class InstallOptions : GLib.Object { return _options_object; } + public unowned Distinst.Disk? get_install_device () { + if (install_device == null) { + install_device = disks.get_disk_with_mount ("/cdrom"); + } + + return install_device; + } + + public unowned string? get_install_device_path () { + if (install_device_path == null) { + unowned Distinst.Disk? install_device = get_install_device (); + if (install_device != null) { + install_device_path = Utils.string_from_utf8 (install_device.get_device_path ()); + } + } + + return install_device_path; + } + public void set_minimum_size (uint64 size) { minimum_size = size; } @@ -118,6 +140,8 @@ public class InstallOptions : GLib.Object { // Transder ownership of the disks to the caller. public Distinst.Disks take_disks () { disks_moved = true; + install_device = null; + install_device_path = null; return (owned) disks; } diff --git a/src/Views/AlongsideView.vala b/src/Views/AlongsideView.vala new file mode 100644 index 000000000..a4133595f --- /dev/null +++ b/src/Views/AlongsideView.vala @@ -0,0 +1,210 @@ +/** + * This view is for selecting a location to install alongside an existing operationg system. + * + * Possible install options on this view are: + * + * - Shrinking the largest existing partition on a disk, if possible. + * - Installing to the largest unused region on a disk, if possible. + */ +public class AlongsideView: OptionsView { + public signal void next_step (bool use_scale, string? os, uint64 free, uint64 total); + + // Whether to use the resize view for choosing a size or not. + public bool set_scale = false; + // The number of free sectors that the selected install option has. + public uint64 selected_free = 0; + // The number of total sectors that the option has. + public uint64 selected_total = 0; + // The OS that is installed to, or may have ownership of, the option. + public string? selected_os; + + // Possible labels that the next button will have, depending on which option is selected. + private string NEXT_LABEL[5]; + + public AlongsideView () { + Object ( + cancellable: true, + artwork: "disks", + title: _("Install Alongside Another OS") + ); + } + + construct { + NEXT_LABEL = new string[5] { + _("Install"), + _("Resize Partition"), + _("Resize OS"), + _("Install Alongside"), + _("Erase and Install"), + }; + + next_button.label = NEXT_LABEL[3]; + next.connect (() => next_step (set_scale, selected_os, selected_free, selected_total)); + show_all (); + } + + // Clears existing options in the view, and creates new installation options. + public void update_options () { + base.clear_options (); + + var options = InstallOptions.get_default (); + + add_alongside_options (); + + if (options.get_options ().has_erase_options ()) { + add_erase_options (); + } + + base.options.show_all (); + base.select_first_option (); + } + + private void add_alongside_options () { + var install_options = InstallOptions.get_default (); + unowned string? install_device = install_options.get_install_device_path (); + + foreach (var option in install_options.get_options ().get_alongside_options ()) { + var device = Utils.string_from_utf8 (option.get_device ()); + + if (install_device != null && install_device == device) { + debug ("skipping %s because it is on the install device\n", device); + continue; + } + + string? os = Utils.string_from_utf8 (option.get_os ()); + os = os == "none" ? null : os; + + var free = option.get_sectors_free (); + var total = option.get_sectors_total (); + var partition = option.get_partition (); + var partition_path = Utils.string_from_utf8 (option.get_path ()); + string logo = Utils.get_distribution_logo_from_alongside (option); + + string label; + string details; + if (partition == -1) { + label = _("Unused space on %s").printf (device); + details = _("%.1f GiB available").printf ((double) free / SECTORS_AS_GIB); + } else { + label = _("%s on %s").printf (os == null ? _("Partition") : os, device); + details = _("Shrink %s (%.1f GiB free)") + .printf ( + partition_path, + (double) free / SECTORS_AS_GIB + ); + } + + base.add_option (logo, label, details, (button) => { + unowned string next_label; + if (partition == -1) { + next_label = NEXT_LABEL[0]; + } else if (os == null) { + next_label = NEXT_LABEL[1]; + } else { + next_label = NEXT_LABEL[2]; + } + + button.key_press_event.connect ((event) => handle_key_press (button, event)); + button.notify["active"].connect (() => { + if (button.active) { + base.options.get_children ().foreach ((child) => { + if (child is Gtk.ToggleButton) { + ((Gtk.ToggleButton)child).active = child == button; + } + }); + + install_options.selected_option = new Distinst.InstallOption () { + tag = Distinst.InstallOptionVariant.ALONGSIDE, + option = (void*) option, + encrypt_pass = null, + sectors = (partition == -1) ? 0 : free - 1 + }; + + set_scale = partition != -1; + selected_os = os; + selected_free = free; + selected_total = total; + next_button.label = next_label; + next_button.sensitive = true; + } else { + next_button.label = NEXT_LABEL[3]; + next_button.sensitive = false; + } + }); + }); + } + } + + private void add_erase_options () { + var install_options = InstallOptions.get_default (); + unowned Distinst.InstallOptions options = install_options.get_updated_options (); + unowned string? install_device = install_options.get_install_device_path (); + + foreach (unowned Distinst.EraseOption disk in options.get_erase_options ()) { + string device_path = Utils.string_from_utf8 (disk.get_device_path ()); + + if (install_device != null && install_device == device_path && !install_options.has_recovery ()) { + continue; + } + + string logo = Utils.string_from_utf8 (disk.get_linux_icon ()); + string label = Utils.string_from_utf8 (disk.get_model ()); + string details = "Erase %s %.1f GiB".printf ( + Utils.string_from_utf8 (disk.get_device_path ()), + (double) disk.get_sectors () / SECTORS_AS_GIB + ); + + base.add_option(logo, label, details, (button) => { + if (disk.meets_requirements ()) { + button.key_press_event.connect ((event) => handle_key_press (button, event)); + button.notify["active"].connect (() => { + if (button.active) { + base.options.get_children ().foreach ((child) => { + if (child is Gtk.ToggleButton) { + ((Gtk.ToggleButton)child).active = child == button; + } + }); + + if (install_options.has_recovery ()) { + var recovery = options.get_recovery_option (); + + install_options.selected_option = new Distinst.InstallOption () { + tag = Distinst.InstallOptionVariant.RECOVERY, + option = (void*) recovery, + encrypt_pass = null + }; + } else { + install_options.selected_option = new Distinst.InstallOption () { + tag = Distinst.InstallOptionVariant.ERASE, + option = (void*) disk, + encrypt_pass = null + }; + } + + set_scale = false; + next_button.label = NEXT_LABEL[4]; + next_button.sensitive = true; + } else { + next_button.sensitive = false; + next_button.label = NEXT_LABEL[3]; + } + }); + } else { + button.sensitive = false; + } + }); + } + + base.sort_sensitive (); + } + + private bool handle_key_press (Gtk.Button button, Gdk.EventKey event) { + if (event.keyval == Gdk.Key.Return) { + button.clicked (); + next_button.clicked (); + return true; + } + + return false; + } +} diff --git a/src/Views/DiskView.vala b/src/Views/DiskView.vala index d33e744de..6c7d57782 100644 --- a/src/Views/DiskView.vala +++ b/src/Views/DiskView.vala @@ -44,8 +44,15 @@ public class Installer.DiskView : OptionsView { var install_options = InstallOptions.get_default (); unowned Distinst.InstallOptions options = install_options.get_updated_options (); + unowned string? install_device = install_options.get_install_device_path (); foreach (unowned Distinst.EraseOption disk in options.get_erase_options ()) { + string device_path = Utils.string_from_utf8 (disk.get_device_path ()); + + if (install_device != null && install_device == device_path && !install_options.has_recovery ()) { + continue; + } + string logo = Utils.string_from_utf8 (disk.get_linux_icon ()); string label = Utils.string_from_utf8 (disk.get_model ()); string details = "%s %.1f GiB".printf ( diff --git a/src/Views/ResizeView.vala b/src/Views/ResizeView.vala new file mode 100644 index 000000000..000573093 --- /dev/null +++ b/src/Views/ResizeView.vala @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2018 elementary, Inc. (https://elementary.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +public class ResizeView : AbstractInstallerView { + private Gtk.SpinButton other_os_entry { get; set; } + private Gtk.SpinButton our_os_entry { get; set; } + + private Gtk.Label our_os_size_label { get; set; } + private Gtk.Label other_os_size_label { get; set; } + private Gtk.Label other_os_label; + private Gtk.Scale scale; + + public uint64 minimum_required { get; set; } + private uint64 minimum; + private uint64 true_minimum; + private uint64 maximum; + private uint64 used; + private uint64 total; + + public signal void next_step (); + + + const double STEPPING = 100 * 2 * 1024; + + public ResizeView (uint64 minimum_size) { + Object ( + cancellable: true, + minimum_required: minimum_size, + artwork: "disks", + title: "" + ); + } + + construct { + var secondary_label = new Gtk.Label ( + _("Each operating system needs space on your device. Drag the handle below to set how much space each operating system gets.") + ); + secondary_label.max_width_chars = 60; + secondary_label.wrap = true; + secondary_label.xalign = 0; + + scale = new Gtk.Scale (Gtk.Orientation.HORIZONTAL, new Gtk.Adjustment (0, 0, 0, STEPPING, STEPPING * 10, STEPPING * 100)); + scale.draw_value = false; + scale.inverted = true; + + scale.show_fill_level = true; + scale.get_style_context ().add_class (Granite.STYLE_CLASS_ACCENT); + + var our_os_label = new Gtk.Label (Utils.get_pretty_name ()); + our_os_label.halign = Gtk.Align.END; + our_os_label.hexpand = true; + + var our_os_label_context = our_os_label.get_style_context (); + our_os_label_context.add_class (Granite.STYLE_CLASS_H3_LABEL); + our_os_label_context.add_class (Granite.STYLE_CLASS_ACCENT); + + our_os_size_label = new Gtk.Label (null); + our_os_size_label.get_style_context().add_class (Gtk.STYLE_CLASS_DIM_LABEL); + our_os_size_label.halign = Gtk.Align.END; + our_os_size_label.use_markup = true; + our_os_size_label.hexpand = true; + + other_os_label = new Gtk.Label (null); + other_os_label.halign = Gtk.Align.START; + other_os_label.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL); + + other_os_size_label = new Gtk.Label (""); + other_os_size_label.get_style_context().add_class (Gtk.STYLE_CLASS_DIM_LABEL); + other_os_size_label.halign = Gtk.Align.START; + other_os_size_label.hexpand = true; + other_os_size_label.use_markup = true; + + var scale_grid = new Gtk.Grid (); + scale_grid.halign = Gtk.Align.FILL; + scale_grid.row_spacing = 6; + + Gtk.SpinButton our_entry; + Gtk.SpinButton other_entry; + + scale_grid.attach (scale, 0, 0, 2, 1); + scale_grid.attach (other_os_label, 0, 1); + scale_grid.attach (our_os_label, 1, 1); + scale_grid.attach (create_entry (out other_entry, Gtk.Align.START), 0, 2); + scale_grid.attach (create_entry (out our_entry, Gtk.Align.END), 1, 2); + scale_grid.attach (other_os_size_label, 0, 3); + scale_grid.attach (our_os_size_label, 1, 3); + + other_os_entry = other_entry; + our_os_entry = our_entry; + + var grid = new Gtk.Grid (); + grid.row_spacing = 12; + grid.valign = Gtk.Align.CENTER; + + grid.attach (secondary_label, 0, 0); + grid.attach (scale_grid, 0, 1); + + content_area.attach (grid, 1, 0, 1, 2); + + var next_button = new Gtk.Button.with_label (_("Resize and Install")); + next_button.can_default = true; + next_button.has_default = true; + next_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + next_button.clicked.connect (() => { + unowned Distinst.InstallOption? selected = InstallOptions.get_default ().get_selected_option(); + if (selected == null) { + critical (_("selected option not found in alongside view")); + return; + } + + selected.sectors = (uint64) scale.get_value (); + next_step (); + }); + + action_area.add (next_button); + update_size_labels ((int) scale.get_value ()); + show_all (); + + bool open = true; + scale.value_changed.connect (() => { + if (open) { + open = false; + + constrain_scale (scale); + double our_size = scale.get_value (); + other_os_entry.set_value (((double) total - our_size) / SECTORS_AS_GIB); + our_os_entry.set_value (our_size / SECTORS_AS_GIB); + + open = true; + } + + update_size_labels ((uint64) scale.get_value ()); + }); + + our_os_entry.value_changed.connect(() => { + if (open) { + open = false; + + constrain_entry (our_os_entry, true_minimum, maximum); + double our_size = our_os_entry.get_value (); + other_os_entry.set_value (((double) total / SECTORS_AS_GIB) - our_size); + scale.set_value (our_size * SECTORS_AS_GIB); + + open = true; + } + }); + + other_os_entry.value_changed.connect(() => { + if (open) { + open = false; + + constrain_entry (other_os_entry, total - maximum, total - true_minimum); + double other_size = other_os_entry.get_value (); + our_os_entry.set_value (((double) total / SECTORS_AS_GIB) - other_size); + scale.set_value ((double) total - (other_size * SECTORS_AS_GIB)); + + open = true; + } + }); + + scale.grab_focus (); + } + + public void update_options (string? os, uint64 free, uint64 total) { + title_label.label = _("Resize %s").printf (os == null ? _("Partition") : _("OS")); + + this.total = total; + used = total - free; + minimum = minimum_required + (2 * 1024); + + const int HEADROOM = 5 * 2 * 1024 * 1024; + + maximum = total - used - InstallOptions.SHRINK_OVERHEAD; + true_minimum = minimum + HEADROOM > maximum ? minimum : minimum + HEADROOM; + + double max_range = (double) total / SECTORS_AS_GIB; + our_os_entry.set_range (0.0, max_range); + our_os_entry.set_increments (0.5, 5); + + other_os_entry.set_range (0.0, max_range); + other_os_entry.set_increments (0.5, 5); + + var quarter = total / 4; + var half = quarter * 2; + var three_quarters = quarter * 3; + + scale.clear_marks (); + scale.set_range (0, total); + scale.add_mark (minimum, Gtk.PositionType.BOTTOM, _("Min")); + + if (quarter < maximum && quarter > minimum) { + scale.add_mark (quarter, Gtk.PositionType.BOTTOM, "25%"); + } + + if (half < maximum && half > minimum) { + scale.add_mark (half, Gtk.PositionType.BOTTOM, "50%"); + } + + if (three_quarters < maximum && three_quarters > minimum) { + scale.add_mark (three_quarters, Gtk.PositionType.BOTTOM, "75%"); + } + + scale.add_mark (maximum, Gtk.PositionType.BOTTOM, _("Max")); + scale.fill_level = total - used; + scale.set_value (total / 2); + + other_os_label.label = os == null ? _("Partition") : os; + } + + private void constrain_scale (Gtk.Scale scale) { + double scale_value = scale.get_value (); + if (scale_value < true_minimum) { + scale.set_value (true_minimum); + } else if (scale_value > maximum) { + scale.set_value (maximum); + } + } + + private void constrain_entry (Gtk.SpinButton entry, uint64 minimum, uint64 maximum) { + double entry_value = entry.get_value (); + double min = minimum / SECTORS_AS_GIB; + double max = maximum / SECTORS_AS_GIB; + + if (entry_value < min) { + entry.set_value (min); + } else if (entry_value > max) { + entry.set_value (max); + } + } + + private void update_size_labels (uint64 our_os_size) { + uint64 other_os_size = total - our_os_size; + + our_os_size_label.label = _("""%s Free""".printf ( + "%.1f GiB".printf ((double) (our_os_size - minimum) / SECTORS_AS_GIB) + )); + + other_os_size_label.label = _("""%s Free""".printf ( + "%.1f GiB".printf ((double) (other_os_size - used) / SECTORS_AS_GIB) + )); + } +} + +Gtk.Box create_entry (out Gtk.SpinButton entry, Gtk.Align alignment) { + entry = new Gtk.SpinButton (null, 1.0, 1); + + var label = new Gtk.Label (_("GiB")); + + var container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); + container.add (entry); + container.add (label); + container.halign = alignment; + + return container; +} diff --git a/src/Views/TryInstallView.vala b/src/Views/TryInstallView.vala index 516a2ba6f..c515ea6cc 100644 --- a/src/Views/TryInstallView.vala +++ b/src/Views/TryInstallView.vala @@ -129,13 +129,6 @@ public class Installer.TryInstallView : AbstractInstallerView { () => refresh_step () ); - // var alongside_button = button_creator.new_button ( - // _("Install Alongside OS"), - // "drive-multidisk", - // _("Install %s next to one or more existing OS installations").printf (pretty_name), - // () => alongside_step () - // ); - var custom_button = button_creator.new_button ( _("Custom (Advanced)"), "disk-utility", @@ -153,23 +146,35 @@ public class Installer.TryInstallView : AbstractInstallerView { var sizegroup = new Gtk.SizeGroup (Gtk.SizeGroupMode.BOTH); sizegroup.add_widget (clean_install_button.type_image); sizegroup.add_widget (refresh_install_button.type_image); - // sizegroup.add_widget (alongside_button.type_image); sizegroup.add_widget (custom_button.type_image); type_grid.add (clean_install_button); type_grid.add (refresh_install_button); - // type_grid.add (alongside_button); - type_grid.add (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); - type_grid.add (custom_button); demo_button.key_press_event.connect ((event) => handle_key_press (demo_button, event)); clean_install_button.key_press_event.connect ((event) => handle_key_press (clean_install_button, event)); refresh_install_button.key_press_event.connect ((event) => handle_key_press (refresh_install_button, event)); - // alongside_button.key_press_event.connect ((event) => handle_key_press (alongside_button, event)); custom_button.key_press_event.connect ((event) => handle_key_press (custom_button, event)); var options = InstallOptions.get_default (); + InstallTypeButton alongside_button = null; + + // Disallow the alongside OS option if the live media is the recovery partition. + if (!options.has_recovery ()) { + alongside_button = button_creator.new_button ( + _("Install Alongside OS"), + "drive-multidisk", + _("Install %s next to one or more existing OS installations").printf (pretty_name), + () => alongside_step () + ); + + sizegroup.add_widget (alongside_button.type_image); + type_grid.add (alongside_button); + alongside_button.key_press_event.connect ((event) => handle_key_press (alongside_button, event)); + alongside_button.visible = options.get_options ().has_alongside_options (); + } + decrypt_button.clicked.connect (() => { var decrypt_dialog = new DecryptDialog (); decrypt_dialog.update_list (); @@ -180,7 +185,10 @@ public class Installer.TryInstallView : AbstractInstallerView { decrypt_dialog.response.connect ((resp) => { if (resp == Gtk.ResponseType.DELETE_EVENT) { refresh_install_button.visible = options.get_options ().has_refresh_options (); - // alongside_button.visible = options.get_options ().has_alongside_options (); + + if (alongside_button != null) { + alongside_button.visible = options.get_options ().has_alongside_options (); + } var nlocked = partitions_locked (); decrypt_infobar.visible = nlocked != 0; @@ -188,15 +196,16 @@ public class Installer.TryInstallView : AbstractInstallerView { }); }); + type_grid.add (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); + type_grid.add (custom_button); + show_all (); clean_install_button.grab_focus (); - // Hide the info bar if no encrypted partitions are found. decrypt_infobar.visible = partitions_locked () != 0; refresh_install_button.visible = options.get_options ().has_refresh_options (); - // alongside_button.visible = options.get_options ().has_alongside_options (); } private bool handle_key_press (Gtk.Button button, Gdk.EventKey event) { diff --git a/src/meson.build b/src/meson.build index 486d228ed..3e0c0ea33 100644 --- a/src/meson.build +++ b/src/meson.build @@ -14,6 +14,7 @@ vala_files = [ 'Objects/InstallOptions.vala', 'Objects/Mount.vala', 'Views/AbstractInstallerView.vala', + 'Views/AlongsideView.vala', 'Views/CheckView.vala', 'Views/DiskView.vala', 'Views/EncryptView.vala', @@ -24,6 +25,7 @@ vala_files = [ 'Views/PartitioningView.vala', 'Views/ProgressView.vala', 'Views/RefreshView.vala', + 'Views/ResizeView.vala', 'Views/SuccessView.vala', 'Views/TryInstallView.vala', 'Widgets/DecryptMenu.vala',