aboutsummaryrefslogtreecommitdiffhomepage
path: root/tensorflow/python/debug/cli
diff options
context:
space:
mode:
authorGravatar Shanqing Cai <cais@google.com>2017-02-07 15:43:46 -0800
committerGravatar TensorFlower Gardener <gardener@tensorflow.org>2017-02-07 16:03:45 -0800
commit8c828561e90b8ed8a814cebec7aa7f802fae7388 (patch)
tree9969571ab4903e81ae5018df3a1bd0c7d2474aeb /tensorflow/python/debug/cli
parentdfe3bb7cdc888767c2fff32efea405c8d6aa5c88 (diff)
tfdbg curses CLI: add mouse-triggered screen scrolling
This can serve as an alternative and more convenient way to scroll screen outputs on environments that lack easy access to PgDown and PgUp keys, e.g., Macbooks and Chromebooks. Change: 146844824
Diffstat (limited to 'tensorflow/python/debug/cli')
-rw-r--r--tensorflow/python/debug/cli/curses_ui.py253
-rw-r--r--tensorflow/python/debug/cli/curses_ui_test.py120
2 files changed, 332 insertions, 41 deletions
diff --git a/tensorflow/python/debug/cli/curses_ui.py b/tensorflow/python/debug/cli/curses_ui.py
index ba17c1632d..dcfde5de1a 100644
--- a/tensorflow/python/debug/cli/curses_ui.py
+++ b/tensorflow/python/debug/cli/curses_ui.py
@@ -31,6 +31,16 @@ from tensorflow.python.debug.cli import debugger_cli_common
from tensorflow.python.debug.cli import tensor_format
+_SCROLL_REFRESH = "refresh"
+_SCROLL_UP = "up"
+_SCROLL_DOWN = "down"
+_SCROLL_UP_A_LINE = "up_a_line"
+_SCROLL_DOWN_A_LINE = "down_a_line"
+_SCROLL_HOME = "home"
+_SCROLL_END = "end"
+_SCROLL_TO_LINE_INDEX = "scroll_to_line_index"
+
+
def _get_command_from_line_attr_segs(mouse_x, attr_segs):
"""Attempt to extract command from the attribute segments of a line.
@@ -51,6 +61,138 @@ def _get_command_from_line_attr_segs(mouse_x, attr_segs):
return attr.content
+class ScrollBar(object):
+ """Vertical ScrollBar for Curses-based CLI.
+
+ An object of this class has knowledge of the location of the scroll bar
+ in the screen coordinates, the current scrolling position, and the total
+ number of text lines in the screen text. By using this information, it
+ can generate text rendering of the scroll bar, which consists of and UP
+ button on the top and a DOWN button on the bottom, in addition to a scroll
+ block in between, whose exact location is determined by the scrolling
+ position. The object can also calculate the scrolling command (e.g.,
+ _SCROLL_UP_A_LINE, _SCROLL_DOWN) from the coordinate of a mouse click
+ event in the screen region it occupies.
+ """
+
+ BASE_ATTR = "black_on_white"
+
+ def __init__(self,
+ min_x,
+ min_y,
+ max_x,
+ max_y,
+ scroll_position,
+ output_num_rows):
+ """Constructor of ScrollBar.
+
+ Args:
+ min_x: (int) left index of the scroll bar on the screen (inclusive).
+ min_y: (int) top index of the scroll bar on the screen (inclusive).
+ max_x: (int) right index of the scroll bar on the screen (inclusive).
+ max_y: (int) bottom index of the scroll bar on the screen (inclusive).
+ scroll_position: (int) 0-based location of the screen output. For example,
+ if the screen output is scrolled to the top, the value of
+ scroll_position should be 0. If it is scrolled to the bottom, the value
+ should be output_num_rows - 1.
+ output_num_rows: (int) Total number of output rows.
+
+ Raises:
+ ValueError: If the width or height of the scroll bar, as determined
+ by min_x, max_x, min_y and max_y, is too small.
+ """
+
+ self._min_x = min_x
+ self._min_y = min_y
+ self._max_x = max_x
+ self._max_y = max_y
+ self._scroll_position = scroll_position
+ self._output_num_rows = output_num_rows
+ self._scroll_bar_height = max_y - min_y + 1
+
+ if self._max_x < self._min_x:
+ raise ValueError("Insufficient width for ScrollBar (%d)" %
+ (self._max_x - self._min_x + 1))
+ if self._max_y < self._min_y + 3:
+ raise ValueError("Insufficient height for ScrollBar (%d)" %
+ (self._max_y - self._min_y + 1))
+
+ def _block_y(self):
+ """Get the 0-based y coordinate of the scroll block.
+
+ This y coordinate takes into account the presence of the UP and DN buttons
+ present at the top and bottom of the ScrollBar. For example, at the home
+ location, the return value will be 1; at the bottom location, the return
+ value will be self._scroll_bar_height - 2.
+
+ Returns:
+ (int) 0-based y coordinate of the scroll block, in the ScrollBar
+ coordinate system, i.e., not the screen coordinate system. For example,
+ when scroll position is at the top, this return value will be 1 (not 0,
+ because of the presence of the UP button). When scroll position is at
+ the bottom, this return value will be self._scroll_bar_height - 2
+ (not self._scroll_bar_height - 1, because of the presence of the DOWN
+ button).
+ """
+
+ return int(float(self._scroll_position) / (self._output_num_rows - 1) *
+ (self._scroll_bar_height - 3)) + 1
+
+ def layout(self):
+ """Get the RichTextLines layout of the scroll bar.
+
+ Returns:
+ (debugger_cli_common.RichTextLines) The text layout of the scroll bar.
+ """
+ width = self._max_x - self._min_x + 1
+ empty_line = " " * width
+ foreground_font_attr_segs = [(0, width, self.BASE_ATTR)]
+
+ if self._output_num_rows > 1:
+ block_y = self._block_y()
+
+ if width == 1:
+ up_text = "U"
+ down_text = "D"
+ elif width == 2:
+ up_text = "UP"
+ down_text = "DN"
+ elif width == 3:
+ up_text = "UP "
+ down_text = "DN "
+ else:
+ up_text = " UP "
+ down_text = "DOWN"
+
+ layout = debugger_cli_common.RichTextLines(
+ [up_text], font_attr_segs={0: [(0, width, self.BASE_ATTR)]})
+ for i in xrange(1, self._scroll_bar_height - 1):
+ font_attr_segs = foreground_font_attr_segs if i == block_y else None
+ layout.append(empty_line, font_attr_segs=font_attr_segs)
+ layout.append(down_text, font_attr_segs=foreground_font_attr_segs)
+ else:
+ layout = debugger_cli_common.RichTextLines(
+ [empty_line] * self._scroll_bar_height)
+
+ return layout
+
+ def get_click_command(self, mouse_y):
+ # TODO(cais): Support continuous scrolling when the mouse button is held
+ # down.
+ if self._output_num_rows <= 1:
+ return None
+ elif mouse_y == self._min_y:
+ return _SCROLL_UP_A_LINE
+ elif mouse_y == self._max_y:
+ return _SCROLL_DOWN_A_LINE
+ elif mouse_y > self._block_y() and mouse_y < self._max_y:
+ return _SCROLL_DOWN
+ elif mouse_y < self._block_y() and mouse_y > self._min_y:
+ return _SCROLL_UP
+ else:
+ return None
+
+
class CursesUI(base_ui.BaseUI):
"""Curses-based Command-line UI.
@@ -63,17 +205,14 @@ class CursesUI(base_ui.BaseUI):
REGEX_SEARCH_PREFIX = "/"
TENSOR_INDICES_NAVIGATION_PREFIX = "@"
+ # Limit screen width to work around the limitation of the curses library that
+ # it may return invalid x coordinates for large values.
+ _SCREEN_WIDTH_LIMIT = 220
+
# Possible Enter keys. 343 is curses key code for the num-pad Enter key when
# num lock is off.
CLI_CR_KEYS = [ord("\n"), ord("\r"), 343]
- _SCROLL_REFRESH = "refresh"
- _SCROLL_UP = "up"
- _SCROLL_DOWN = "down"
- _SCROLL_HOME = "home"
- _SCROLL_END = "end"
- _SCROLL_TO_LINE_INDEX = "scroll_to_line_index"
-
_FOREGROUND_COLORS = {
"white": curses.COLOR_WHITE,
"red": curses.COLOR_RED,
@@ -193,7 +332,7 @@ class CursesUI(base_ui.BaseUI):
# Size of view port on screen, which is always smaller or equal to the
# screen size.
self._output_pad_screen_height = self._output_num_rows - 1
- self._output_pad_screen_width = self._max_x - 1
+ self._output_pad_screen_width = self._max_x - 2
self._output_pad_screen_location = self.rectangle(
top=self._output_top_row,
left=0,
@@ -392,6 +531,8 @@ class CursesUI(base_ui.BaseUI):
def _screen_refresh_size(self):
self._max_y, self._max_x = self._stdscr.getmaxyx()
+ if self._max_x > self._SCREEN_WIDTH_LIMIT:
+ self._max_x = self._SCREEN_WIDTH_LIMIT
def _dispatch_command(self, command):
"""Dispatch user command.
@@ -440,7 +581,7 @@ class CursesUI(base_ui.BaseUI):
if not omitted:
self._scroll_output(
- self._SCROLL_TO_LINE_INDEX, line_index=line_index)
+ _SCROLL_TO_LINE_INDEX, line_index=line_index)
except Exception as e: # pylint: disable=broad-except
self._error_toast(str(e))
else:
@@ -531,16 +672,16 @@ class CursesUI(base_ui.BaseUI):
self._textbox_curr_terminator = self.CLI_TAB_KEY
return self.CLI_TERMINATOR_KEY
elif x == curses.KEY_PPAGE:
- self._scroll_output(self._SCROLL_UP)
+ self._scroll_output(_SCROLL_UP_A_LINE)
return x
elif x == curses.KEY_NPAGE:
- self._scroll_output(self._SCROLL_DOWN)
+ self._scroll_output(_SCROLL_DOWN_A_LINE)
return x
elif x == curses.KEY_HOME:
- self._scroll_output(self._SCROLL_HOME)
+ self._scroll_output(_SCROLL_HOME)
return x
elif x == curses.KEY_END:
- self._scroll_output(self._SCROLL_END)
+ self._scroll_output(_SCROLL_END)
return x
elif x in [curses.KEY_UP, curses.KEY_DOWN]:
# Command history navigation.
@@ -582,11 +723,18 @@ class CursesUI(base_ui.BaseUI):
mouse_event_type = None
if mouse_event_type == curses.BUTTON1_RELEASED:
- command = self._fetch_hyperlink_command(mouse_x, mouse_y)
- if command:
- self._auto_key_in(command)
- self._textbox_curr_terminator = x
- return self.CLI_TERMINATOR_KEY
+ # Logic for mouse-triggered scrolling.
+ if mouse_x >= self._max_x - 2:
+ scroll_command = self._scroll_bar.get_click_command(mouse_y)
+ if scroll_command is not None:
+ self._scroll_output(scroll_command)
+ return x
+ else:
+ command = self._fetch_hyperlink_command(mouse_x, mouse_y)
+ if command:
+ self._auto_key_in(command)
+ self._textbox_curr_terminator = x
+ return self.CLI_TERMINATOR_KEY
else:
# Mark the pending command as modified.
self._textbox_pending_command_changed = True
@@ -702,7 +850,7 @@ class CursesUI(base_ui.BaseUI):
# Wrap the output lines according to screen width.
self._curr_wrapped_output, wrapped_line_indices = (
- debugger_cli_common.wrap_rich_text_lines(output, self._max_x - 1))
+ debugger_cli_common.wrap_rich_text_lines(output, self._max_x - 2))
# Append lines to curr_wrapped_output so that the user can scroll to a
# state where the last text line is on the top of the output area.
@@ -787,16 +935,16 @@ class CursesUI(base_ui.BaseUI):
if next_match_line >= 0:
self._scroll_output(
- self._SCROLL_TO_LINE_INDEX, line_index=next_match_line)
+ _SCROLL_TO_LINE_INDEX, line_index=next_match_line)
else:
# Regex search found no match >= current line number. Display message
# stating as such.
self._toast("Pattern not found", color=self._ERROR_TOAST_COLOR_PAIR)
elif is_refresh:
- self._scroll_output(self._SCROLL_REFRESH)
+ self._scroll_output(_SCROLL_REFRESH)
else:
self._output_pad_row = 0
- self._scroll_output(self._SCROLL_HOME)
+ self._scroll_output(_SCROLL_HOME)
def _display_lines(self, output, min_num_rows):
"""Display RichTextLines object on screen.
@@ -825,7 +973,7 @@ class CursesUI(base_ui.BaseUI):
# Size of the output pad, which may exceed screen size and require
# scrolling.
- cols = self._max_x - 1
+ cols = self._max_x - 2
# Create new output pad.
pad = self._screen_new_output_pad(rows, cols)
@@ -853,12 +1001,12 @@ class CursesUI(base_ui.BaseUI):
debugger_cli_common.MAIN_MENU_KEY].format_as_single_line(
prefix="| ", divider=" | ", enabled_item_attrs=["underline"])
- self._main_menu_pad = self._screen_new_output_pad(1, self._max_x - 1)
+ self._main_menu_pad = self._screen_new_output_pad(1, self._max_x - 2)
# The unwrapped menu line may exceed screen width, in which case it needs
# to be cut off.
wrapped_menu, _ = debugger_cli_common.wrap_rich_text_lines(
- self._main_menu, self._max_x - 2)
+ self._main_menu, self._max_x - 3)
self._screen_add_line_to_output_pad(
self._main_menu_pad,
0,
@@ -940,12 +1088,25 @@ class CursesUI(base_ui.BaseUI):
screen_location_left, screen_location_bottom,
screen_location_right)
+ self._scroll_bar = ScrollBar(
+ self._max_x - 2,
+ 2,
+ self._max_x - 1,
+ self._output_num_rows,
+ self._output_pad_row,
+ self._output_pad_height - self._output_pad_screen_height)
+
+ (scroll_pad, _, _) = self._display_lines(
+ self._scroll_bar.layout(), self._output_num_rows - 1)
+ scroll_pad.refresh(
+ 0, 0, 2, self._max_x - 2, self._output_num_rows, self._max_x - 1)
+
def _scroll_output(self, direction, line_index=None):
"""Scroll the output pad.
Args:
- direction: _SCROLL_REFRESH, _SCROLL_UP, _SCROLL_DOWN, _SCROLL_HOME or
- _SCROLL_END, _SCROLL_TO_LINE_INDEX
+ direction: _SCROLL_REFRESH, _SCROLL_UP, _SCROLL_DOWN, _SCROLL_UP_A_LINE,
+ _SCROLL_DOWN_A_LINE, _SCROLL_HOME, _SCROLL_END, _SCROLL_TO_LINE_INDEX
line_index: (int) Specifies the zero-based line index to scroll to.
Applicable only if direction is _SCROLL_TO_LINE_INDEX.
@@ -959,28 +1120,40 @@ class CursesUI(base_ui.BaseUI):
# No output pad is present. Do nothing.
return
- if direction == self._SCROLL_REFRESH:
+ if direction == _SCROLL_REFRESH:
pass
- elif direction == self._SCROLL_UP:
- # Scroll up
+ elif direction == _SCROLL_UP:
+ # Scroll up.
+ self._output_pad_row -= int(self._output_num_rows / 3)
+ if self._output_pad_row < 0:
+ self._output_pad_row = 0
+ elif direction == _SCROLL_DOWN:
+ # Scroll down.
+ self._output_pad_row += int(self._output_num_rows / 3)
+ if (self._output_pad_row >
+ self._output_pad_height - self._output_pad_screen_height - 1):
+ self._output_pad_row = (
+ self._output_pad_height - self._output_pad_screen_height - 1)
+ elif direction == _SCROLL_UP_A_LINE:
+ # Scroll up a line
if self._output_pad_row - 1 >= 0:
self._output_pad_row -= 1
- elif direction == self._SCROLL_DOWN:
- # Scroll down
+ elif direction == _SCROLL_DOWN_A_LINE:
+ # Scroll down a line
if self._output_pad_row + 1 < (
self._output_pad_height - self._output_pad_screen_height):
self._output_pad_row += 1
- elif direction == self._SCROLL_HOME:
+ elif direction == _SCROLL_HOME:
# Scroll to top
self._output_pad_row = 0
- elif direction == self._SCROLL_END:
+ elif direction == _SCROLL_END:
# Scroll to bottom
self._output_pad_row = (
self._output_pad_height - self._output_pad_screen_height - 1)
- elif direction == self._SCROLL_TO_LINE_INDEX:
+ elif direction == _SCROLL_TO_LINE_INDEX:
if not isinstance(line_index, int):
raise TypeError("Invalid line_index type (%s) under mode %s" %
- (type(line_index), self._SCROLL_TO_LINE_INDEX))
+ (type(line_index), _SCROLL_TO_LINE_INDEX))
self._output_pad_row = line_index
else:
raise ValueError("Unsupported scroll mode: %s" % direction)
@@ -1183,7 +1356,7 @@ class CursesUI(base_ui.BaseUI):
if self._curr_unwrapped_output:
# Force refresh screen output.
- self._scroll_output(self._SCROLL_REFRESH)
+ self._scroll_output(_SCROLL_REFRESH)
if not candidates:
return
@@ -1197,7 +1370,7 @@ class CursesUI(base_ui.BaseUI):
})
candidates_output, _ = debugger_cli_common.wrap_rich_text_lines(
- candidates_output, self._max_x - 2)
+ candidates_output, self._max_x - 3)
# Calculate how many lines the candidate text should occupy. Limit it to
# a maximum value.
@@ -1210,7 +1383,7 @@ class CursesUI(base_ui.BaseUI):
pad, _, _ = self._display_lines(candidates_output, 0)
self._screen_scroll_output_pad(
pad, 0, 0, self._candidates_top_row, 0,
- self._candidates_top_row + candidates_num_rows - 1, self._max_x - 1)
+ self._candidates_top_row + candidates_num_rows - 1, self._max_x - 2)
def _toast(self, message, color=None, line_index=None):
"""Display a one-line message on the screen.
@@ -1229,7 +1402,7 @@ class CursesUI(base_ui.BaseUI):
message, font_attr_segs={0: [(0, len(message), color or "white")]}),
0)
- right_end = min(len(message), self._max_x - 1)
+ right_end = min(len(message), self._max_x - 2)
if line_index is None:
line_index = self._output_scroll_row - 1
diff --git a/tensorflow/python/debug/cli/curses_ui_test.py b/tensorflow/python/debug/cli/curses_ui_test.py
index 922d6c5739..cfad01f17c 100644
--- a/tensorflow/python/debug/cli/curses_ui_test.py
+++ b/tensorflow/python/debug/cli/curses_ui_test.py
@@ -878,7 +878,7 @@ class CursesTest(test_util.TensorFlowTestCase):
def testRegexSearchUnderLineWrapping(self):
ui = MockCursesUI(
40,
- 5, # Use a narrow window to trigger line wrapping
+ 6, # Use a narrow window to trigger line wrapping
command_sequence=[
string_to_codes("babble -n 3 -l foo-bar-baz-qux\n"),
string_to_codes("/foo\n"), # Regex search and highlight.
@@ -1369,5 +1369,123 @@ class CursesTest(test_util.TensorFlowTestCase):
self.assertEqual(["bar"] * 10, ui.unwrapped_outputs[0].lines)
+class ScrollBarTest(test_util.TensorFlowTestCase):
+
+ def testConstructorRaisesExceptionForNotEnoughHeight(self):
+ with self.assertRaisesRegexp(
+ ValueError, r"Insufficient height for ScrollBar \(2\)"):
+ curses_ui.ScrollBar(0, 0, 1, 1, 0, 0)
+
+ def testLayoutIsEmptyForZeroRow(self):
+ scroll_bar = curses_ui.ScrollBar(0, 0, 1, 7, 0, 0)
+ layout = scroll_bar.layout()
+ self.assertEqual([" "] * 8, layout.lines)
+ self.assertEqual({}, layout.font_attr_segs)
+
+ def testLayoutIsEmptyFoOneRow(self):
+ scroll_bar = curses_ui.ScrollBar(0, 0, 1, 7, 0, 1)
+ layout = scroll_bar.layout()
+ self.assertEqual([" "] * 8, layout.lines)
+ self.assertEqual({}, layout.font_attr_segs)
+
+ def testClickCommandForOneRowIsNone(self):
+ scroll_bar = curses_ui.ScrollBar(0, 0, 1, 7, 0, 1)
+ self.assertIsNone(scroll_bar.get_click_command(0))
+ self.assertIsNone(scroll_bar.get_click_command(3))
+ self.assertIsNone(scroll_bar.get_click_command(7))
+ self.assertIsNone(scroll_bar.get_click_command(8))
+
+ def testLayoutIsCorrectForTopPosition(self):
+ scroll_bar = curses_ui.ScrollBar(0, 0, 1, 7, 0, 20)
+ layout = scroll_bar.layout()
+ self.assertEqual(["UP"] + [" "] * 6 + ["DN"], layout.lines)
+ self.assertEqual(
+ {0: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)],
+ 1: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)],
+ 7: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)]},
+ layout.font_attr_segs)
+
+ def testWidth1LayoutIsCorrectForTopPosition(self):
+ scroll_bar = curses_ui.ScrollBar(0, 0, 0, 7, 0, 20)
+ layout = scroll_bar.layout()
+ self.assertEqual(["U"] + [" "] * 6 + ["D"], layout.lines)
+ self.assertEqual(
+ {0: [(0, 1, curses_ui.ScrollBar.BASE_ATTR)],
+ 1: [(0, 1, curses_ui.ScrollBar.BASE_ATTR)],
+ 7: [(0, 1, curses_ui.ScrollBar.BASE_ATTR)]},
+ layout.font_attr_segs)
+
+ def testWidth3LayoutIsCorrectForTopPosition(self):
+ scroll_bar = curses_ui.ScrollBar(0, 0, 2, 7, 0, 20)
+ layout = scroll_bar.layout()
+ self.assertEqual(["UP "] + [" "] * 6 + ["DN "], layout.lines)
+ self.assertEqual(
+ {0: [(0, 3, curses_ui.ScrollBar.BASE_ATTR)],
+ 1: [(0, 3, curses_ui.ScrollBar.BASE_ATTR)],
+ 7: [(0, 3, curses_ui.ScrollBar.BASE_ATTR)]},
+ layout.font_attr_segs)
+
+ def testWidth4LayoutIsCorrectForTopPosition(self):
+ scroll_bar = curses_ui.ScrollBar(0, 0, 3, 7, 0, 20)
+ layout = scroll_bar.layout()
+ self.assertEqual([" UP "] + [" "] * 6 + ["DOWN"], layout.lines)
+ self.assertEqual(
+ {0: [(0, 4, curses_ui.ScrollBar.BASE_ATTR)],
+ 1: [(0, 4, curses_ui.ScrollBar.BASE_ATTR)],
+ 7: [(0, 4, curses_ui.ScrollBar.BASE_ATTR)]},
+ layout.font_attr_segs)
+
+ def testLayoutIsCorrectForBottomPosition(self):
+ scroll_bar = curses_ui.ScrollBar(0, 0, 1, 7, 19, 20)
+ layout = scroll_bar.layout()
+ self.assertEqual(["UP"] + [" "] * 6 + ["DN"], layout.lines)
+ self.assertEqual(
+ {0: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)],
+ 6: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)],
+ 7: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)]},
+ layout.font_attr_segs)
+
+ def testLayoutIsCorrectForMiddlePosition(self):
+ scroll_bar = curses_ui.ScrollBar(0, 0, 1, 7, 10, 20)
+ layout = scroll_bar.layout()
+ self.assertEqual(["UP"] + [" "] * 6 + ["DN"], layout.lines)
+ self.assertEqual(
+ {0: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)],
+ 3: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)],
+ 7: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)]},
+ layout.font_attr_segs)
+
+ def testClickCommandsAreCorrectForMiddlePosition(self):
+ scroll_bar = curses_ui.ScrollBar(0, 0, 1, 7, 10, 20)
+ self.assertIsNone(scroll_bar.get_click_command(-1))
+ self.assertEqual(curses_ui._SCROLL_UP_A_LINE,
+ scroll_bar.get_click_command(0))
+ self.assertEqual(curses_ui._SCROLL_UP,
+ scroll_bar.get_click_command(1))
+ self.assertEqual(curses_ui._SCROLL_UP,
+ scroll_bar.get_click_command(2))
+ self.assertIsNone(scroll_bar.get_click_command(3))
+ self.assertEqual(curses_ui._SCROLL_DOWN,
+ scroll_bar.get_click_command(5))
+ self.assertEqual(curses_ui._SCROLL_DOWN,
+ scroll_bar.get_click_command(6))
+ self.assertEqual(curses_ui._SCROLL_DOWN_A_LINE,
+ scroll_bar.get_click_command(7))
+ self.assertIsNone(scroll_bar.get_click_command(8))
+
+ def testClickCommandsAreCorrectForBottomPosition(self):
+ scroll_bar = curses_ui.ScrollBar(0, 0, 1, 7, 19, 20)
+ self.assertIsNone(scroll_bar.get_click_command(-1))
+ self.assertEqual(curses_ui._SCROLL_UP_A_LINE,
+ scroll_bar.get_click_command(0))
+ for i in range(1, 6):
+ self.assertEqual(curses_ui._SCROLL_UP,
+ scroll_bar.get_click_command(i))
+ self.assertIsNone(scroll_bar.get_click_command(6))
+ self.assertEqual(curses_ui._SCROLL_DOWN_A_LINE,
+ scroll_bar.get_click_command(7))
+ self.assertIsNone(scroll_bar.get_click_command(8))
+
+
if __name__ == "__main__":
googletest.main()