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