# Disk resizing dialog # # Copyright (C) 2012 Red Hat, Inc. # # This copyrighted material is made available to anyone wishing to use, # modify, copy, or redistribute it subject to the terms and conditions of # the GNU General Public License v.2, or (at your option) any later version. # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY expressed or implied, including the implied warranties 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, write to the # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the # source code or documentation are not subject to the GNU General Public # License and may only be used or replicated with the express permission of # Red Hat, Inc. # # Red Hat Author(s): Chris Lumens # from __future__ import division from gi.repository import Gtk from pyanaconda.ui.gui import GUIObject from pyanaconda.storage.size import Size import gettext _ = lambda x: gettext.ldgettext("anaconda", x) N_ = lambda x: x P_ = lambda x, y, z: gettext.ldngettext("anaconda", x, y, z) __all__ = ["ResizeDialog"] DEVICE_ID_COL = 0 DESCRIPTION_COL = 1 FILESYSTEM_COL = 2 RECLAIMABLE_COL = 3 PERCENT_COL = 4 ACTION_COL = 5 EDITABLE_COL = 6 TOOLTIP_COL = 7 PRESERVE = N_("Preserve") SHRINK = N_("Shrink") DELETE = N_("Delete") def size_str(mb): if isinstance(mb, Size): spec = str(mb) else: spec = "%s mb" % mb return str(Size(spec=spec)).upper() class ResizeDialog(GUIObject): builderObjects = ["actionStore", "diskStore", "resizeDialog"] mainWidgetName = "resizeDialog" uiFile = "spokes/lib/resize.glade" def __init__(self, data, storage, payload): GUIObject.__init__(self, data) self.storage = storage self.payload = payload self._actionStore = self.builder.get_object("actionStore") self._diskStore = self.builder.get_object("diskStore") self._view = self.builder.get_object("diskView") self._diskStore = self.builder.get_object("diskStore") self._reclaimable_label = self.builder.get_object("reclaimableSpaceLabel") self._selected_label = self.builder.get_object("selectedSpaceLabel") self._required_label = self.builder.get_object("requiredSpaceLabel") markup = self._required_label.get_label() self._required_label.set_markup(markup % size_str(self.payload.spaceRequired)) self._reclaimDescLabel = self.builder.get_object("reclaimDescLabel") self._resizeButton = self.builder.get_object("resizeButton") self._preserveButton = self.builder.get_object("preserveButton") # self._shrinkButton = self.builder.get_object("shrinkButton") self._deleteButton = self.builder.get_object("deleteButton") def _description(self, part): # First, try to find the partition in some known Root. If we find # it, return the mountpoint as the description. for root in self.storage.roots: for (mount, device) in root.mounts.iteritems(): if device == part: return "%s (%s)" % (mount, root.name) # Otherwise, fall back on increasingly vague information. if not part.isleaf: return self.storage.devicetree.getChildren(part)[0].name if getattr(part.format, "label", None): return part.format.label elif getattr(part.format, "name", None): return part.format.name else: return "" def populate(self, disks): totalDisks = 0 totalReclaimableSpace = 0 self._selectedReclaimableSpace = 0 canShrinkSomething = False for disk in disks: # First add the disk itself. if disk.partitioned: editable = False percent = 0 fstype = "" diskReclaimableSpace = 0 else: editable = not disk.protected percent = 100 fstype = disk.format.type diskReclaimableSpace = disk.size itr = self._diskStore.append(None, [disk.id, "%s %s" % (size_str(disk.size), disk.description), fstype, "%s total", percent, _(PRESERVE), editable, _("Whole disks are not editable.")]) if disk.partitioned: # Then add all its partitions. for dev in self.storage.devicetree.getChildren(disk): if dev.isExtended and disk.format.logicalPartitions: continue # Devices that are not resizable are still deletable. if dev.resizable: freeSize = dev.size - dev.minSize if not dev.protected: canShrinkSomething = True else: freeSize = dev.size if dev.protected: tooltip = _("This device contains the installation source.") else: tooltip = None self._diskStore.append(itr, [dev.id, self._description(dev), dev.format.type, _("%s of %s") % (size_str(freeSize), size_str(dev.size)), int((freeSize/dev.size) * 100), _(PRESERVE), not dev.protected, tooltip]) diskReclaimableSpace += freeSize # And then go back and fill in the total reclaimable space for the # disk, now that we know what each partition has reclaimable. self._diskStore[itr][RECLAIMABLE_COL] = self._diskStore[itr][RECLAIMABLE_COL] % size_str(diskReclaimableSpace) self._diskStore[itr][PERCENT_COL] = int((diskReclaimableSpace/disk.size)*100) totalDisks += 1 totalReclaimableSpace += diskReclaimableSpace self._update_labels(totalDisks, totalReclaimableSpace, 0) description = _("You don't have enough free space available for this installation.\n\n" "You can remove existing filesystems you no longer need to free up space " "for this installation. Removing a filesystem will permanently delete all " "of the data it contains.") # if canShrinkSomething: # description += "\n\n" # description += _("There is also free space available in pre-existing filesystems. " # "While it's risky and we recommend you back up your data first, you " # "can recover that free disk space and make it available for this " # "installation below.") self._reclaimDescLabel.set_text(description) def _update_labels(self, nDisks=None, totalReclaimable=None, selectedReclaimable=None): if nDisks is not None and totalReclaimable is not None: text = P_("%s disk; %s reclaimable space (in filesystems)", "%s disks; %s reclaimable space (in filesystems)", nDisks) % (nDisks, size_str(totalReclaimable)) self._reclaimable_label.set_markup(text) if selectedReclaimable is not None: text = _("Total selected space to reclaim: %s") % size_str(selectedReclaimable) self._selected_label.set_markup(text) def _update_buttons(self, row): # If this is a disk header, it's not editable, so make all the # buttons insensitive. self._preserveButton.set_sensitive(row[EDITABLE_COL]) # self._shrinkButton.set_sensitive(row[EDITABLE_COL]) self._deleteButton.set_sensitive(row[EDITABLE_COL]) if not row[EDITABLE_COL]: return # If the selected filesystem does not support shrinking, make that # button insensitive. device = self.storage.devicetree.getDeviceByID(row[DEVICE_ID_COL]) # self._shrinkButton.set_sensitive(device.resizable) # Then, disable the button for whatever action is currently selected. # It doesn't make a lot of sense to allow clicking that. if row[ACTION_COL] == _(PRESERVE): self._preserveButton.set_sensitive(False) # elif row[ACTION_COL] == _(SHRINK): # self._shrinkButton.set_sensitive(False) elif row[ACTION_COL] == _(DELETE): self._deleteButton.set_sensitive(False) def refresh(self, disks): super(ResizeDialog, self).refresh() # clear out the store and repopulate it from the devicetree self._diskStore.clear() self.populate(disks) self._view.expand_all() def run(self): rc = self.window.run() self.window.destroy() return rc # Signal handlers. def _sumReclaimableSpace(self, model, path, itr, *args): (editable, action, ident) = model.get(itr, EDITABLE_COL, ACTION_COL, DEVICE_ID_COL) if not editable: return False device = self.storage.devicetree.getDeviceByID(ident) if action == _(PRESERVE): return False if action == _(SHRINK): if device.resizable: self._selectedReclaimableSpace += (device.size - device.minSize) else: self._selectedReclaimableSpace += device.size elif action == _(DELETE): self._selectedReclaimableSpace += device.size return False def on_preserve_clicked(self, button): self._actionChanged(PRESERVE) def on_shrink_clicked(self, button): self._actionChanged(SHRINK) def on_delete_clicked(self, button): self._actionChanged(DELETE) def _actionChanged(self, newAction): selection = self.builder.get_object("diskView-selection") (model, itr) = selection.get_selected() if not itr: return selectedRow = self._diskStore[itr] # Put the selected action into the store as well. selectedRow[ACTION_COL] = _(newAction) # And then we're keeping a running tally of how much space the user # has selected to reclaim, so reflect that in the UI. self._selectedReclaimableSpace = 0 self._diskStore.foreach(self._sumReclaimableSpace, None) self._update_labels(selectedReclaimable=self._selectedReclaimableSpace) got = Size(spec="%s MB" % self._selectedReclaimableSpace) need = self.payload.spaceRequired self._resizeButton.set_sensitive(got >= need) self._update_buttons(selectedRow) def _scheduleActions(self, model, path, itr, *args): (editable, action, ident) = model.get(itr, EDITABLE_COL, ACTION_COL, DEVICE_ID_COL) device = self.storage.devicetree.getDeviceByID(ident) if not editable: return False if action == _(PRESERVE): return False elif action == _(SHRINK): if device.resizable: self.storage.resizeDevice(device, device.minSize) else: self.storage.recursiveRemove(device) elif action == _(DELETE): self.storage.recursiveRemove(device) return False def on_resize_clicked(self, *args): self._diskStore.foreach(self._scheduleActions, None) def on_row_clicked(self, view, path, column): # This handles when the user clicks on a row in the view. We use it # only for expanding/collapsing disk headers. if view.row_expanded(path): view.collapse_row(path) else: view.expand_row(path, True) def on_selection_changed(self, selection): # This handles when the selection changes. It's very similar to what # on_row_clicked above does, but this handler only deals with changes in # selection. Thus, clicking on a disk header to collapse it and then # immediately clicking on it again to expand it would not work when # dealt with here. (model, itr) = selection.get_selected() if not itr: return self._update_buttons(self._diskStore[itr])