def deep_refresh(self, y_loc, x_loc):
        '''Refresh the actual physical location on the screen of the part
        of the pad that's visible

        '''

        self.latest_yx = (y_loc, x_loc)
        self.no_ut_refresh(y_loc, x_loc)
        InnerWindow.deep_refresh(self, y_loc, x_loc)
    def deep_refresh(self, y_loc, x_loc):
        '''Refresh the actual physical location on the screen of the part
        of the pad that's visible

        '''

        self.latest_yx = (y_loc, x_loc)
        self.no_ut_refresh(y_loc, x_loc)
        InnerWindow.deep_refresh(self, y_loc, x_loc)
 def setUp(self):
     '''unit test set up
      Sets several functions to call do_nothing to allow
      test execution in non-curses environment. 
     '''
     self.inner_window_init_win = InnerWindow._init_win
     self.inner_window_set_color = InnerWindow.set_color
     InnerWindow._init_win = do_nothing
     InnerWindow.set_color = do_nothing
     self.win = InnerWindow(WindowArea(60, 70, 0, 0),
                            color_theme=ColorTheme(force_bw=True))
     self.win.window = MockWin()
     for x in range(5):
         self.win.add_object(MockWin())
 def init_status_bar(self, y_loc, x_loc, width):
     '''Initialize the progress bar window and set to 0%'''
     self.status_bar_width = width
     status_bar_area = WindowArea(1, width + 3, y_loc, x_loc + 1)
     self.status_bar = InnerWindow(status_bar_area, window=self.center_win)
     self.status_bar.window.addch(0, 0, InstallProgress.PROG_BAR_ENDS[0])
     self.status_bar.window.addch(0, width + 1,
                                  InstallProgress.PROG_BAR_ENDS[1])
     self.progress_color = self.center_win.color_theme.progress_bar
     self.set_status_percent(0)
    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()
class TestInnerWindow(unittest.TestCase):
    def setUp(self):
        '''unit test set up
         Sets several functions to call do_nothing to allow
         test execution in non-curses environment. 
        '''
        self.inner_window_init_win = InnerWindow._init_win
        self.inner_window_set_color = InnerWindow.set_color
        InnerWindow._init_win = do_nothing
        InnerWindow.set_color = do_nothing
        self.win = InnerWindow(WindowArea(60, 70, 0, 0),
                               color_theme=ColorTheme(force_bw=True))
        self.win.window = MockWin()
        for x in range(5):
            self.win.add_object(MockWin())

    def tearDown(self):
        '''unit test tear down
        Functions originally saved in setUp are restored to their
        original values.
        '''
        InnerWindow._init_win = self.inner_window_init_win
        InnerWindow.set_color = self.inner_window_set_color

    def test_activate_jump_idx_less_than_zero(self):
        '''InnerWindow.activate_object(idx, jump=True) for negative idx
           activates the first object'''
        self.assertEquals(self.win.active_object, None)

        self.win.activate_object(-5, jump=True)
        self.assertEquals(0, self.win.active_object)
        self.assertTrue(self.win.objects[0].active)

    def test_activate_jump_idx_gt_length(self):
        '''InnerWindow.activate_object(idx, jump=True)
           for idx > len(InnerWindow.objects) activates last object'''
        self.assertEquals(self.win.active_object, None)

        self.win.activate_object(len(self.win.objects) + 10, jump=True)
        self.assertEquals(len(self.win.objects) - 1, self.win.active_object)
        self.assertTrue(self.win.objects[-1].active)

    def test_on_page(self):
        '''InnerWindow.on_page() activates correct object and returns None'''
        self.win.activate_object(0)
        ret = self.win.on_page(1, 12345)
        self.assertEquals(None, ret)
        self.assertTrue(self.win.objects[-1].active)

    def test_on_page_no_change(self):
        '''InnerWindow.on_page() returns input_key when active
           object is unchanged'''
        self.win.activate_object(0)
        ret = self.win.on_page(-1, 12345)
        self.assertEquals(ret, 12345)
        self.assertTrue(self.win.objects[0].active)

    def test_on_page_no_active(self):
        '''InnerWindow.on_page() returns input_key when
           there is no active_object'''
        self.assertEquals(self.win.active_object, None)

        ret = self.win.on_page(1, 12345)
        self.assertEquals(ret, 12345)
        self.assertEquals(self.win.active_object, None)

    def test_escape_sequences(self):
        '''Test correct terminal input escape sequence processing'''
        self.assertEquals(esc_seq('[17~'), curses.KEY_F6)
        self.assertEquals(esc_seq('2'), curses.KEY_F2)
        self.assertEquals(esc_seq('OH'), curses.KEY_HOME)
        self.assertEquals(esc_seq('X'), None)  # invalid
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