Пример #1
0
class DiskWindow(InnerWindow):
    '''Display and edit disk information, including partitions and slices'''

    STATIC_PARTITION_HEADERS = [(12, _("Primary"), _("Logical")),
                                (10, _(" Size(GB)"), _(" Size(GB)"))]

    EDIT_PARTITION_HEADERS = [(13, _("Primary"), _("Logical")),
                              (10, _(" Size(GB)"), _(" Size(GB)")),
                              (7, _(" Avail"), _(" Avail"))]

    STATIC_SLICE_HEADERS = [(13, _("Slice"), _("Slice")),
                            (2, "#", "#"),
                            (10, _(" Size(GB)"), _(" Size(GB)"))]

    EDIT_SLICE_HEADERS = [(13, _("Slice"), _("Slice")),
                          (2, "#", "#"),
                          (10, _(" Size(GB)"), _(" Size(GB)")),
                          (7, _(" Avail"), _(" Avail"))]

    ADD_KEYS = {curses.KEY_LEFT: no_action,
                curses.KEY_RIGHT: no_action}

    DEAD_ZONE = 3
    SCROLL_PAD = 2

    MIN_SIZE = None
    REC_SIZE = None

    SIZE_PRECISION = Size(UI_PRECISION).get(Size.gb_units)

    DESTROYED_MARK = EditField.ASTERISK_CHAR

    def __init__(self, area, disk_info, editable=False,
                 error_win=None, target_controller=None, **kwargs):
        '''See also InnerWindow.__init__

        disk_info (required) - Either a Disk or Partition object
        containing the data to be represented. If a Partition objects is
        provided, it will be used for displaying slice
        data within that partition. If Disk has partition(s), those are
        displayed. If not, but it has slices, then those are displayed. If
        neither partition data nor slice data are available, a ValueError is
        raised.

        headers (required) - List of tuples to populate the header of this
        window with. The first item in each tuple should be the width of the
        header, the second item should be the left side header.

        editable (optional) - If True, the window will be created such that
        data is editable.

        target_controller(optional) - Target controller

        '''

        global LOGGER
        LOGGER = logging.getLogger(INSTALL_LOGGER_NAME)

        self.headers = None
        self.orig_ext_part_field = None
        self.orig_logicals_active = False
        self.ext_part_field = None
        self.error_win = error_win
        self.editable = editable
        self.win_width = None
        self.left_win = None
        self.right_win = None
        self.list_area = None
        self.edit_area = None
        super(DiskWindow, self).__init__(area, add_obj=editable, **kwargs)
        self.left_header_string = None
        self.right_header_string = None
        self._orig_data = None
        self.disk_info = None
        self.has_partition_data = False
        self.key_dict[curses.KEY_LEFT] = self.on_arrow_key
        self.key_dict[curses.KEY_RIGHT] = self.on_arrow_key
        if self.editable:
            self.key_dict[curses.KEY_F5] = self.change_type

        self.tc = target_controller
        self._ui_obj = None
        self.ui_obj = disk_info

        self.set_disk_info(ui_obj=self.ui_obj)

        LOGGER.debug(self.ui_obj)

        if platform.processor() == "sparc":
            self.is_x86 = False
        else:
            self.is_x86 = True

    @property
    def ui_obj(self):
        return self._ui_obj

    @ui_obj.setter
    def ui_obj(self, part):
        ''' create and set the value for ui_obj depending on type '''
        if isinstance(part, Disk):
            self._ui_obj = UIDisk(self.tc, parent=None, doc_obj=part)
        elif isinstance(part, Partition):
            self._ui_obj = UIPartition(self.tc, parent=None, doc_obj=part)
        else:
            # Must be a either a Disk or Partition.  It's an error to be here
            raise RuntimeError("disk_info object is invalid")

    def _init_win(self, window):
        '''Require at least 70 columns and 6 lines to fit current needs for
        display of partitions and slices. Builds two inner ScrollWindows for
        displaying/editing the data.

        '''
        if self.area.columns < 70:
            raise ValueError("Insufficient space - area.columns < 70")
        if self.area.lines < 6:
            raise ValueError("Insufficient space - area.lines < 6")
        self.win_width = (self.area.columns - DiskWindow.DEAD_ZONE
                          + DiskWindow.SCROLL_PAD) / 2

        super(DiskWindow, self)._init_win(window)

        win_area = WindowArea(self.area.lines - 1, self.win_width, 2, 0)
        win_area.scrollable_lines = self.area.lines - 2
        self.left_win = ScrollWindow(win_area, window=self, add_obj=False)
        self.left_win.color = None
        self.left_win.highlight_color = None
        win_area.x_loc = self.win_width + DiskWindow.DEAD_ZONE
        win_area.scrollable_lines = 2 * MAX_EXT_PARTS
        self.right_win = ScrollWindow(win_area, window=self, add_obj=False)
        self.right_win.color = None
        self.right_win.highlight_color = None

    def set_disk_info(self, ui_obj=None, disk_info=None, no_part_ok=False):
        '''Set up this DiskWindow to represent disk_info'''

        if ui_obj is not None:
            disk_info = ui_obj.doc_obj
        elif disk_info is not None:
            self.ui_obj = disk_info
        else:
            # Should never be this case
            raise RuntimeError("Unable to find ui_obj or disk_info")

        part_list = disk_info.get_children(class_type=Partition)
        if part_list:
            self.has_partition_data = True
        else:
            slice_list = disk_info.get_children(class_type=Slice)
            if slice_list:
                self.has_partition_data = False
            else:
                # No partitions and no slices
                if no_part_ok:
                    if self.is_x86:
                        self.has_partition_data = True
                    else:
                        self.has_partition_data = False
                else:
                    return

        if self.has_partition_data:
            if self.editable:
                self.headers = DiskWindow.EDIT_PARTITION_HEADERS
                self.list_area = WindowArea(1, self.headers[0][0] +
                                            self.headers[1][0],
                                            0, DiskWindow.SCROLL_PAD)
                self.edit_area = WindowArea(1, self.headers[1][0], 0,
                                            self.headers[0][0])
            else:
                self.headers = DiskWindow.STATIC_PARTITION_HEADERS
        else:
            if self.editable:
                self.headers = DiskWindow.EDIT_SLICE_HEADERS
                self.list_area = WindowArea(1, self.headers[0][0] +
                                            self.headers[1][0] +
                                            self.headers[2][0],
                                            0, DiskWindow.SCROLL_PAD)
                self.edit_area = WindowArea(1, self.headers[2][0], 0,
                                            self.headers[0][0] +
                                            self.headers[1][0])
            else:
                self.headers = DiskWindow.STATIC_SLICE_HEADERS

        LOGGER.debug("have_partition: %s", self.has_partition_data)
        LOGGER.debug(self.ui_obj)

        self.ui_obj.add_unused_parts(no_part_ok=no_part_ok)

        self.left_win.clear()
        self.right_win.clear()
        self.window.erase()
        self.print_headers()

        if self.editable:
            self.active_object = None
            self.build_edit_fields()
            self.right_win.bottom = max(0, len(self.right_win.all_objects) -
                                        self.right_win.area.lines)
            if self.has_partition_data:
                self.orig_ext_part_field = None
                for obj in self.left_win.objects:
                    if (obj.data_obj.is_extended()):
                        self.orig_ext_part_field = obj
                        self.orig_logicals_active = True
                        break
        else:
            self.print_data()

    def print_headers(self):
        '''Print the headers for the displayed data.

        header[0] - The width of this column. header[1] and header[2] are
                    trimmed to this size
        header[1] - The internationalized text for the left window
        header[2] - The internationalized text for the right window

        '''
        self.left_header_string = []
        self.right_header_string = []
        for header in self.headers:
            left_header_str = header[1]
            right_header_str = header[2]
            # Trim the header to fit in the column width,
            # splitting columns with at least 1 space
            # Pad with extra space(s) to align the columns
            left_header_str = fit_text_truncate(left_header_str,
                                                header[0] - 1, just="left")
            self.left_header_string.append(left_header_str)
            right_header_str = fit_text_truncate(right_header_str,
                                                header[0] - 1, just="left")
            self.right_header_string.append(right_header_str)
        self.left_header_string = " ".join(self.left_header_string)
        self.right_header_string = " ".join(self.right_header_string)
        LOGGER.debug(self.left_header_string)
        self.add_text(self.left_header_string, 0, DiskWindow.SCROLL_PAD)
        right_win_offset = (self.win_width + DiskWindow.DEAD_ZONE +
                            DiskWindow.SCROLL_PAD)
        self.add_text(self.right_header_string, 0, right_win_offset)
        self.window.hline(1, DiskWindow.SCROLL_PAD, curses.ACS_HLINE,
                          textwidth(self.left_header_string))
        self.window.hline(1, right_win_offset, curses.ACS_HLINE,
                          textwidth(self.right_header_string))
        self.no_ut_refresh()

    def print_data(self):
        '''Print static (non-editable) data.

        Slices - fill the left side, then remaining slices on the right side.
        If for some reason not all slices fit, indicate how many more slices
        there area

        Partitions - Put standard partitions on the left, logical partitions
        on the right

        '''

        part_index = 0
        data = self.ui_obj.get_parts_in_use()

        if len(data) == 0:
            return   # should never be this case

        if self.has_partition_data:
            max_parts = MAX_PRIMARY_PARTS
        else:
            max_parts = min(len(data), self.left_win.area.lines)

        win = self.left_win
        y_loc = 0
        for next_data in data:
            LOGGER.debug("next_data: %s", next_data)
            if y_loc >= max_parts:
                if win is self.left_win:
                    win = self.right_win
                    y_loc = 0
                    max_parts = win.area.lines
                else:
                    num_extra = len(data) - part_index
                    if self.has_partition_data:
                        more_parts_txt = _("%d more partitions") % num_extra
                    else:
                        more_parts_txt = _("%d more slices") % num_extra
                    win.add_text(more_parts_txt, win.area.lines, 3)
                    break
            x_loc = DiskWindow.SCROLL_PAD
            field = 0
            win.add_text(next_data.get_description(), y_loc, x_loc,
                         self.headers[field][0] - 1)
            x_loc += self.headers[field][0]
            field += 1
            if not self.has_partition_data:
                win.add_text(str(next_data.name), y_loc, x_loc,
                             self.headers[field][0] - 1)
                x_loc += self.headers[field][0]
                field += 1
            win.add_text(locale.format("%*.1f", (self.headers[field][0] - 1,
                next_data.size.get(Size.gb_units))), y_loc, x_loc,
                self.headers[field][0] - 1)
            x_loc += self.headers[field][0]
            y_loc += 1
            field += 1
            part_index += 1
        self.right_win.use_vert_scroll_bar = False
        self.no_ut_refresh()

    def build_edit_fields(self):
        '''Build subwindows for editing partition sizes

        For slices, fill the left side, then the right (right side scrolling as
        needed, though this shouldn't happen unless the number of slices on
        disk exceeds 8 for some reason)

        For partitions, fill the left side up to MAX_PRIMARY_PARTS,
        and place all logical partitions on the right.

        '''

        data = self.ui_obj.get_parts_in_use()

        if self.has_partition_data:
            max_left_parts = MAX_PRIMARY_PARTS
        else:
            if len(data) == 0:
                return   # should never be this case
            max_left_parts = min(len(data), self.left_win.area.lines)

        part_iter = iter(data)
        try:
            next_part = part_iter.next()
            self.objects.append(self.left_win)
            for y_loc in range(max_left_parts):
                self.list_area.y_loc = y_loc
                self.create_list_item(next_part, self.left_win, self.list_area)
                next_part = part_iter.next()
            self.objects.append(self.right_win)
            for y_loc in range(self.right_win.area.scrollable_lines):
                self.list_area.y_loc = y_loc
                self.create_list_item(next_part, self.right_win,
                                      self.list_area)
                next_part = part_iter.next()
            if len(data) > max_left_parts:
                self.right_win.use_vert_scroll_bar = True
        except StopIteration:
            if len(self.right_win.all_objects) <= self.right_win.area.lines:
                self.right_win.use_vert_scroll_bar = False
            self.right_win.no_ut_refresh()
        else:
            raise ValueError("Could not fit all partitions in DiskWindow")
        self.no_ut_refresh()

    def create_list_item(self, next_part, win, list_area):
        '''Add an entry for next_part (a Partition or Slice) to
        the DiskWindow

        '''
        list_item = ListItem(list_area, window=win, data_obj=next_part)
        list_item.key_dict.update(DiskWindow.ADD_KEYS)
        edit_field = EditField(self.edit_area, window=list_item,
                               numeric_pad=" ",
                               validate=decimal_valid,
                               on_exit=on_exit_edit,
                               error_win=self.error_win,
                               add_obj=False,
                               data_obj=next_part)
        edit_field.right_justify = True
        edit_field.validate_kwargs["disk_win"] = self
        edit_field.on_exit_kwargs["disk_win"] = self
        edit_field.key_dict.update(DiskWindow.ADD_KEYS)
        self.update_part(part_field=list_item)
        return list_item

    def update_part(self, part_info=None, part_field=None):
        '''Sync changed partition data to the screen.'''
        if part_field is None:
            if part_info is None:
                raise ValueError("Must supply either part_info or part_field")
            part_field = self.find_part_field(part_info)[1]
        elif part_info is None:
            part_info = part_field.data_obj
        elif part_field.data_obj is not part_info:
            raise ValueError("part_field must be a ListItem associated with "
                             "part_info")
        if not isinstance(part_field, ListItem):
            raise TypeError("part_field must be a ListItem associated with "
                            "part_info")
        if self.has_partition_data:
            desc_text = part_info.get_description()
        else:
            desc_length = self.headers[0][0] - 1
            desc_text = "%-*.*s %s" % (desc_length, desc_length,
                                       part_info.get_description(),
                                       part_info.name)
        part_field.set_text(desc_text)
        edit_field = part_field.all_objects[0]
        edit_field.set_text(locale.format("%.1f",
                                          part_info.size.get(Size.gb_units)))
        self.mark_if_destroyed(part_field)
        self._update_edit_field(part_info, part_field, edit_field)

        self.update_avail_space(part_info=part_info)
        if self.has_partition_data:
            if part_info.is_extended():
                self.ext_part_field = part_field

    def _update_edit_field(self, part_info, part_field, edit_field):
        '''If the partition/slice is editable, add it to the .objects list.
        If it's also the part_field that's currently selected, then activate
        the edit field.

        '''
        if part_info.editable():
            part_field.objects = [edit_field]
            active_win = self.get_active_object()
            if active_win is not None:
                if active_win.get_active_object() is part_field:
                    part_field.activate_object(edit_field)
        else:
            edit_field.make_inactive()
            part_field.objects = []
            part_field.active_object = None

    def mark_if_destroyed(self, part_field):
        '''Determine if the partition/slice represented by part_field has
        changed such that its contents will be destroyed.

        '''
        part_info = part_field.data_obj
        destroyed = part_info.modified()
        self.mark_destroyed(part_field, destroyed)

    def mark_destroyed(self, part_field, destroyed):
        '''If destroyed is True, add an asterisk indicating that the
        partition or slice's content will be destroyed during installation.
        Otherwise, clear the asterisk

        '''
        y_loc = part_field.area.y_loc
        x_loc = part_field.area.x_loc - 1
        if part_field in self.right_win.objects:
            win = self.right_win
        else:
            win = self.left_win
        if destroyed:
            win.window.addch(y_loc, x_loc, DiskWindow.DESTROYED_MARK,
                             win.color_theme.inactive)
        else:
            win.window.addch(y_loc, x_loc, InnerWindow.BKGD_CHAR)

    def update_avail_space(self, part_number=None, part_info=None):
        '''Update the 'Avail' column for the specified slice or partition.
        If no number is given, all avail columns are updated

        '''
        if part_number is None and part_info is None:
            self._update_all_avail_space()
        else:
            self._update_avail_space(part_number, part_info)

    def _update_all_avail_space(self):
        '''Update the 'Avail' column for all slices or partitions.'''
        idx = 0
        for item in self.left_win.objects:
            self.update_avail_space(idx)
            idx += 1
        for item in self.right_win.objects:
            self.update_avail_space(idx)
            idx += 1
        y_loc = idx - len(self.left_win.objects)
        if self.has_partition_data:
            x_loc = self.headers[0][0] + self.headers[1][0] + 1
            field = 2
        else:
            x_loc = (self.headers[0][0] + self.headers[1][0] +
                     self.headers[2][0] + 1)
            field = 3
        if y_loc > 0:
            self.right_win.add_text(" " * self.headers[field][0],
                                    y_loc, x_loc)

    def _update_avail_space(self, part_number=None, part_info=None):
        '''Update the 'Avail' column for the specified slice or partition.'''
        if part_number is None:
            win, item = self.find_part_field(part_info)
        elif part_number < len(self.left_win.objects):
            win = self.left_win
            item = win.objects[part_number]
        else:
            win = self.right_win
            item = win.objects[part_number - len(self.left_win.objects)]
        if self.has_partition_data:
            x_loc = self.headers[0][0] + self.headers[1][0] + 1
            field = 2
        else:
            x_loc = (self.headers[0][0] + self.headers[1][0] +
                     self.headers[2][0] + 1)
            field = 3
        y_loc = item.area.y_loc
        part = item.data_obj
        max_space = part.get_max_size()
        max_space = locale.format("%*.1f", (self.headers[field][0],
                                             max_space.get(Size.gb_units)))
        win.add_text(max_space, y_loc, x_loc)

    def find_part_field(self, part_info):
        '''Given a PartitionInfo or SliceInfo object, find the associated
        ListItem. This search compares by reference, and will only succeed
        if you have a handle to the exact object referenced by the ListItem

        '''
        for win in [self.left_win, self.right_win]:
            for item in win.objects:
                if item.data_obj is part_info:
                    return win, item
        raise ValueError("Part field not found")

    def reset(self, dummy=None):
        '''Reset ui_obj to value found from Target Discovery.
        Meaningful only for editable DiskWindows

        '''
        if not self.editable:
            return
        doc = InstallEngine.get_instance().doc

        # "reset" the desired target
        reset_obj = None
        if isinstance(self.ui_obj, UIDisk):
            reset_obj = (self.tc.reset_layout(disk=self.ui_obj.doc_obj))[0]
        else:
            # reset the partition by removing the modified Partition, and
            # resetting it with the partition found during target discovery.

            discovered_obj = self.ui_obj.discovered_doc_obj

            desired_disk = get_desired_target_disk(doc)
            desired_part = get_solaris_partition(doc)

            desired_disk.delete_partition(desired_part)
            part_copy = deepcopy(discovered_obj)
            desired_disk.insert_children(part_copy)

            # get the updated reference
            reset_obj = get_solaris_partition(doc)

        dump_doc("After doing reset")

        self.set_disk_info(disk_info=reset_obj)
        self.activate_solaris_data()

    def activate_solaris_data(self):
        '''Find the Solaris Partition / ZFS Root Pool Slice and activate it.

        '''

        if self.editable:
            solaris_part = self.ui_obj.get_solaris_data()
            if solaris_part is None:
                LOGGER.debug("No Solaris data, activating default")
                self.activate_object()
                self.right_win.scroll(scroll_to_line=0)
                return
            disk_order = self.ui_obj.get_parts_in_use().index(solaris_part)
            LOGGER.debug("solaris disk at disk_order = %s", disk_order)
            self.activate_index(disk_order)

    def make_active(self):
        '''On activate, select the solaris partition or ZFS root pool,
        instead of defaulting to 0

        '''
        self.set_color(self.highlight_color)
        self.activate_solaris_data()

    def on_arrow_key(self, input_key):
        '''
        On curses.KEY_LEFT: Move from the right win to the left win
        On curses.KEY_RIGHT: Move from the left to the right

        '''
        if (input_key == curses.KEY_LEFT and
            self.get_active_object() is self.right_win and
            len(self.left_win.objects) > 0):

            active_object = self.right_win.get_active_object().area.y_loc
            if (active_object >= len(self.left_win.objects)):
                active_object = len(self.left_win.objects) - 1
            self.activate_object(self.left_win)
            self.left_win.activate_object(active_object)
            return None
        elif (input_key == curses.KEY_RIGHT and
              self.get_active_object() is self.left_win and
              len(self.right_win.objects) > 0):
            active_line = (self.left_win.active_object +
                             self.right_win.current_line[0])
            active_object = None
            force_to_top = False
            for obj in self.right_win.objects:
                if obj.area.y_loc >= active_line:
                    active_object = obj
                    off_screen = (self.right_win.current_line[0] +
                                  self.right_win.area.lines)
                    if active_object.area.y_loc > off_screen:
                        force_to_top = True
                    break
            if active_object is None:
                active_object = 0
            self.left_win.activate_object(-1, loop=True)
            self.activate_object(self.right_win)
            self.right_win.activate_object_force(active_object,
                                                 force_to_top=force_to_top)
            return None
        return input_key

    def no_ut_refresh(self, abs_y=None, abs_x=None):
        '''Refresh self, left win and right win explicitly'''
        super(DiskWindow, self).no_ut_refresh()
        self.left_win.no_ut_refresh(abs_y, abs_x)
        self.right_win.no_ut_refresh(abs_y, abs_x)

    def change_type(self, dummy):
        '''Cycle the type for the currently active object, and
        update its field

        '''
        LOGGER.debug("changing type")

        part_field = self.get_active_object().get_active_object()
        part_info = part_field.data_obj

        part_order = self.ui_obj.get_parts_in_use().index(part_info)

        old_obj = part_info.discovered_doc_obj
        old_type = list()
        if old_obj is not None:
            if self.has_partition_data:
                old_type.append(old_obj.part_type)
            else:
                if old_obj.in_zpool is not None:
                    old_type.append(old_obj.in_zpool)
                else:
                    in_use = part_info.doc_obj.in_use
                    if in_use is not None:
                        if in_use['used_name']:
                            old_type.append((in_use['used_name'])[0])

        LOGGER.debug("extra type to cycle: %s", old_type)
        part_info.cycle_type(extra_type=old_type)
        self.set_disk_info(ui_obj=self.ui_obj, no_part_ok=True)
        self.activate_index(part_order)

        return None

    def create_extended(self, ext_part_field):
        '''If this is the original extended partition, restore the original
        logical partitions. Otherwise, create a single unused logical
        partition.

        '''
        if not ext_part_field.data_obj.modified():
            self.right_win.clear()
            self.orig_logicals_active = True
            logicals = deepcopy(self._orig_data.get_logicals())
            self.disk_info.partitions.extend(logicals)
            for idx, logical in enumerate(logicals):
                self.list_area.y_loc = idx
                self.create_list_item(logical, self.right_win, self.list_area)
            if self.right_win not in self.objects:
                self.objects.append(self.right_win)
            self.right_win.activate_object_force(0, force_to_top=True)
            self.right_win.make_inactive()
            self.right_win.no_ut_refresh()
        else:
            # Leave old data be, create new Unused logical partition
            if self.right_win not in self.objects:
                self.objects.append(self.right_win)
            self.append_unused_logical()

    def activate_index(self, obj_index):
        '''Activate the object at the specified index '''

        if obj_index < len(self.left_win.objects):
            LOGGER.debug("activating in left_win")
            self.left_win.activate_object(obj_index)
            self.activate_object(self.left_win)
            self.right_win.scroll(scroll_to_line=0)
        else:
            activate = obj_index - len(self.left_win.objects)
            LOGGER.debug('activating in right win')
            self.right_win.activate_object_force(activate, force_to_top=True)
            self.activate_object(self.right_win)
            left_active = self.left_win.get_active_object()
            if left_active is not None:
                left_active.make_inactive()

    def append_unused_logical(self):
        '''Adds a single Unused logical partition to the right window'''
        new_part = self.disk_info.append_unused_logical()
        self.list_area.y_loc = len(self.right_win.all_objects)
        bottom = self.list_area.y_loc - self.right_win.area.lines + 1
        self.right_win.bottom = max(0, bottom)
        self.create_list_item(new_part, self.right_win, self.list_area)
        scroll = len(self.right_win.all_objects) > self.right_win.area.lines
        self.right_win.use_vert_scroll_bar = scroll
        self.right_win.no_ut_refresh()
Пример #2
0
class MainWindow(object):
    '''Represent initscr (the whole screen), and break it into a border,
    header, and central region. Map F# keystrokes to Actions

    '''
    def __init__(self,
                 initscr,
                 screen_list,
                 default_actions,
                 theme=None,
                 force_bw=False):
        '''Set the theme, and call reset to initialize the terminal to
        prepare for the first screen.

        '''

        if theme is not None:
            self.theme = theme
        else:
            self.theme = ColorTheme(force_bw=force_bw)
        self.screen_list = screen_list
        self.initscr = initscr
        self.default_cursor_pos = (initscr.getmaxyx()[0] - 1, 0)
        self.cursor_pos = self.default_cursor_pos
        self.footer = None
        self.header = None
        self._cur_header_text = None
        self.central_area = None
        self.popup_win = None
        self.error_line = None
        self._active_win = None
        self.actions = None

        # _default_actions keeps a "pristine" copy of the actions
        self._default_actions = default_actions

        # default_actions is copied from _default_actions and may
        # get modified during the course of display of a screen.
        # reset_actions() is responsible for copying the pristine copy
        # into this variable.
        self.default_actions = None
        self.reset()

    def redrawwin(self):
        '''Completely repaint the screen'''
        self.header.redrawwin()
        self.footer.redrawwin()
        self.error_line.redrawwin()
        self.central_area.redrawwin()
        if self._active_win is self.popup_win:
            self.popup_win.redrawwin()

    def do_update(self):
        '''Wrapper to curses.doupdate()'''
        curses.setsyx(*self.get_cursor_loc())
        curses.doupdate()

    def get_cursor_loc(self):
        '''Retrieve the current cursor position from the active UI
        element.

        '''
        cursor = self.central_area.get_cursor_loc()
        if cursor is None:
            cursor = self.cursor_pos
        return cursor

    def reset(self):
        '''Create the InnerWindows representing the header, footer/border,
        error line, and main central_area

        '''
        window_size = self.initscr.getmaxyx()
        win_size_y = window_size[0]
        win_size_x = window_size[1]
        footer_area = WindowArea(1, win_size_x, win_size_y - 1, 0)
        self.footer = InnerWindow(footer_area,
                                  color_theme=self.theme,
                                  color=self.theme.border)
        top = self.initscr.derwin(1, win_size_x, 0, 0)
        left = self.initscr.derwin(win_size_y - 2, 1, 1, 0)
        right = self.initscr.derwin(win_size_y - 2, 1, 1, win_size_x - 1)
        self.footer.more_windows = [top, left, right]
        self.footer.set_color(self.theme.border)
        header_area = WindowArea(1, win_size_x - 2, 1, 1)
        self.header = InnerWindow(header_area,
                                  color_theme=self.theme,
                                  color=self.theme.header)
        central_win_area = WindowArea(win_size_y - 4, win_size_x - 2, 2, 1)
        self.central_area = InnerWindow(central_win_area,
                                        border_size=(0, 2),
                                        color_theme=self.theme)
        self._active_win = self.central_area
        popup_win_area = WindowArea(central_win_area.lines - 10,
                                    central_win_area.columns - 20, 5, 10,
                                    central_win_area.lines - 10)
        self.popup_win = ScrollWindow(popup_win_area,
                                      window=self.central_area,
                                      color=self.theme.error_msg,
                                      highlight_color=self.theme.error_msg)
        error_area = WindowArea(1, win_size_x - 2, win_size_y - 2, 1)
        self.error_line = ErrorWindow(error_area, color_theme=self.theme)
        self.reset_actions()

    def reset_actions(self):
        '''Reset the actions to the defaults, clearing any custom actions
        registered by individual screens

        '''
        # A shallow copy of each Action is desired to properly preserve
        # the Action's reference to a given bound method.
        actions = [copy.copy(action) for action in self._default_actions]
        self.default_actions = actions
        self.set_default_actions()

    @property
    def continue_action(self):
        return self.actions[curses.KEY_F2]

    @property
    def back_action(self):
        return self.actions[curses.KEY_F3]

    @property
    def help_action(self):
        return self.actions[curses.KEY_F6]

    @property
    def quit_action(self):
        return self.actions[curses.KEY_F9]

    def clear(self):
        '''Clear all InnerWindows and reset_actions()'''
        self.header.clear()
        self.footer.clear()
        self.central_area.clear()
        self.error_line.clear_err()
        self.reset_actions()

    def set_header_text(self, header_text):
        '''Set the header_text'''
        text = center_columns(header_text, self.header.area.columns - 1)
        self.header.add_text(text)
        self._cur_header_text = text

    def set_default_actions(self):
        '''Clear the actions dictionary and add the default actions back
        into it

        '''
        self.actions = {}
        for action in self.default_actions:
            self.actions[action.key] = action

    def show_actions(self):
        '''Read through the actions dictionary, displaying all the actions
        descriptive text along the footer (along with a prefix linked to
        its associated keystroke)

        '''
        self.footer.window.clear()
        if InnerWindow.USE_ESC:
            prefix = " Esc-"
        else:
            prefix = "  F"
        strings = []
        length = 0
        action_format = "%s%i_%s"
        for key in sorted(self.actions.keys()):
            key_num = key - curses.KEY_F0
            action_text = self.actions[key].text
            action_str = action_format % (prefix, key_num, action_text)
            strings.append(action_str)
        display_str = "".join(strings)
        max_len = self.footer.window.getmaxyx()[1]
        length = textwidth(display_str)
        if not InnerWindow.USE_ESC:
            length += (len(" Esc-") - len("  F")) * len(self.actions)
        if length > max_len:
            raise ValueError("Can't display footer actions - string too long")
        self.footer.window.addstr(display_str.encode(get_encoding()))
        self.footer.window.noutrefresh()

    def getch(self, redraw_keys=[InnerWindow.REPAINT_KEY]):
        '''Call down into central_area to get a keystroke, and, if necessary,
        update the footer to switch to using the Esc- prefixes.
        Redraw the screen if any of redraw keys is pressed.

        '''
        input_key = self._active_win.getch()
        # Redraw whole screen if one of 'redraw' keys has been pressed.
        if input_key in redraw_keys:
            self.redrawwin()
            input_key = None
        if InnerWindow.UPDATE_FOOTER:
            InnerWindow.UPDATE_FOOTER = False
            self.show_actions()
        return input_key

    def process_input(self, current_screen):
        '''Read input until a keystroke that fires a screen change
        is caught

        '''
        input_key = None
        while input_key not in self.actions:
            input_key = self.getch(current_screen.redraw_keys)
            input_key = self.central_area.process(input_key)
            self.do_update()
        return self.actions[input_key].do_action(current_screen)

    def pop_up(self,
               header,
               question,
               left_btn_txt,
               right_btn_txt,
               color=None):
        '''Suspend the current screen, setting the header
        to 'header', presenting the 'question,' and providing two 'buttons'.
        Returns True if the RIGHT button is selected, False if the LEFT is
        selected. The LEFT button is initially selected.

        '''

        # Hide the cursor, storing its previous state (visibility) so
        # it can be restored when finished. Then, move the cursor
        # to the default position (in case this terminal type does not support
        # hiding the cursor entirely)
        try:
            old_cursor_state = curses.curs_set(0)
        except curses.error:
            old_cursor_state = 2
        cursor_loc = curses.getsyx()
        curses.setsyx(self.cursor_pos[0], self.cursor_pos[1])

        # Add the header, a border, and the question to the window
        self.popup_win.window.border()
        header_x = (self.popup_win.area.columns - textwidth(header)) / 2
        self.popup_win.add_text(header, 0, header_x)
        y_loc = 2
        y_loc += self.popup_win.add_paragraph(question, y_loc, 2)
        y_loc += 2

        # Set the background color based on the parameter given, or choose
        # a default based on the theme. Set the highlight_color by flipping
        # the A_REVERSE bit of the color
        if color is None:
            color = self.popup_win.color
        self.popup_win.set_color(color)
        highlight_color = color ^ curses.A_REVERSE

        # Create two "buttons" of equal size by finding the larger of the
        # two, and centering them
        max_len = max(textwidth(left_btn_txt), textwidth(right_btn_txt))
        left_btn_txt = " [ %s ]" % left_btn_txt.center(max_len)
        right_btn_txt = " [ %s ]" % right_btn_txt.center(max_len)
        button_len = textwidth(left_btn_txt) + 1
        win_size = self.popup_win.window.getmaxyx()
        left_area = WindowArea(1, button_len, y_loc,
                               (win_size[1] / 2) - (button_len + 2))
        left_button = ListItem(left_area,
                               window=self.popup_win,
                               text=left_btn_txt,
                               color=color,
                               highlight_color=highlight_color)
        right_area = WindowArea(1, button_len, y_loc, win_size[1] / 2 + 2)
        right_button = ListItem(right_area,
                                window=self.popup_win,
                                text=right_btn_txt,
                                color=color,
                                highlight_color=highlight_color)

        # Highlight the left button, clear any errors on the screen,
        # and display the pop up
        self.popup_win.activate_object(left_button)
        self.popup_win.no_ut_refresh()
        self.error_line.clear_err()
        self.do_update()

        self._active_win = self.popup_win
        # Loop until the user selects an option.
        input_key = None
        while input_key != curses.KEY_ENTER:
            input_key = self.getch()
            input_key = self.popup_win.process(input_key)
            if input_key == curses.KEY_LEFT:
                self.popup_win.activate_object(left_button)
            elif input_key == curses.KEY_RIGHT:
                self.popup_win.activate_object(right_button)
            self.do_update()
        self._active_win = self.central_area
        user_selected = (self.popup_win.get_active_object() is right_button)

        # Clear the pop up and restore the previous screen, including the
        # cursor position and visibility
        self.popup_win.clear()
        self.central_area.redrawwin()
        curses.setsyx(cursor_loc[0], cursor_loc[1])
        try:
            curses.curs_set(old_cursor_state)
        except curses.error:
            pass
        self.do_update()

        return user_selected