[PATCH 07/13] Add documentation to the simpleline library for TUI
Martin Sivak
msivak at redhat.com
Wed Aug 8 08:52:45 UTC 2012
---
pyanaconda/ui/tui/simpleline/base.py | 294 +++++++++++++++++++++++++------
pyanaconda/ui/tui/simpleline/widgets.py | 87 ++++++++-
2 files changed, 323 insertions(+), 58 deletions(-)
diff --git a/pyanaconda/ui/tui/simpleline/base.py b/pyanaconda/ui/tui/simpleline/base.py
index 573f28b..0c9f750 100644
--- a/pyanaconda/ui/tui/simpleline/base.py
+++ b/pyanaconda/ui/tui/simpleline/base.py
@@ -19,19 +19,47 @@
# Red Hat Author(s): Martin Sivak <msivak at redhat.com>
#
-__all__ = ["ExitMainLoop", "App", "UIScreen", "Widget"]
+__all__ = ["App", "UIScreen", "Widget"]
import readline
class ExitAllMainLoops(Exception):
+ """This exceptions ends the whole App mainloop structure. App.run() quits
+ after it is processed."""
pass
class ExitMainLoop(Exception):
+ """This eceptions ends the outermost mainloop. Used internally when dialogs
+ close."""
pass
class App(object):
+ """This is the main class for TUI screen handling. It is responsible for
+ mainloop control and keeping track of the screen stack.
+
+ Screens are organized in stack structure so it is possible to return
+ to caller when dialog or sub-screen closes.
+
+ It supports four window transitions:
+ - show new screen replacing the current one (linear progression)
+ - show new screen keeping the current one in stack (hub & spoke)
+ - show new screen and wait for it to end (dialog)
+ - close current window and return to the next one in stack
+ """
+
def __init__(self, title, yes_or_no_question = None, width = 80):
+ """
+ :param title: application title for whenewer we need to display app name
+ :type title: unicode
+
+ :param yes_or_no_question: UIScreen object class used for Quit dialog
+ :type yes_or_no_question: class UIScreen accepting additional message arg
+
+ :param width: screen width for rendering purposes
+ :type width: int
+ """
+
self._header = title
self._spacer = "\n".join(2*[width*"="])
self._width = width
@@ -40,36 +68,90 @@ class App(object):
# screen stack contains triplets
# UIScreen to show
# arguments for it's show method
- # value indicating whether new mainloop is needed - None = do nothing, True = execute, False = already running, exit when window closes
+ # value indicating whether new mainloop is needed
+ # - None = do nothing
+ # - True = execute new loop
+ # - False = already running loop, exit when window closes
self._screens = []
def switch_screen(self, ui, args = None):
- """Schedules a screen to replace the current one."""
+ """Schedules a screen to replace the current one.
+
+ :param ui: screen to show
+ :type ui: instance of UIScreen
+
+ :param args: optional argument to pass to ui's refresh method (can be used to select what item should be displayed or so)
+ :type args: anything
+
+ """
+
oldscr, oldattr, oldloop = self._screens.pop()
+
+ # we have to keep the oldloop value so we stop
+ # dialog's mainloop if it ever uses switch_screen
self._screens.append((ui, args, oldloop))
self.redraw()
def switch_screen_with_return(self, ui, args = None):
- """Schedules a screen to show, but keeps the current one in stack to return to, when the new one is closed."""
+ """Schedules a screen to show, but keeps the current one in stack
+ to return to, when the new one is closed.
+
+ :param ui: screen to show
+ :type ui: UIScreen instance
+
+ :param args: optional argument, please see switch_screen for details
+ :type args: anything
+ """
+
self._screens.append((ui, args, None))
self.redraw()
def switch_screen_modal(self, ui, args = None):
- """Starts a new screen right away, so the caller can collect data back. When the new screen is closed, the caller is redisplayed."""
+ """Starts a new screen right away, so the caller can collect data back.
+ When the new screen is closed, the caller is redisplayed.
+
+ This method does not return until the new screen is closed.
+
+ :param ui: screen to show
+ :type ui: UIScreen instance
+
+ :param args: optional argument, please see switch_screen for details
+ :type args: anything
+ """
+
+ # set the third item to True so new loop gets started
self._screens.append((ui, args, True))
self._do_redraw()
def schedule_screen(self, ui, args = None):
- """Add screen to the bottom of the stack."""
+ """Add screen to the bottom of the stack. This is mostly usefull
+ at the beginning to prepare the first screen hierarchy to display.
+
+ :param ui: screen to show
+ :type ui: UIScreen instance
+
+ :param args: optional argument, please see switch_screen for details
+ :type args: anything
+ """
self._screens.insert(0, (ui, args, None))
def close_screen(self, scr = None):
+ """Close the currently displayed screen and exit it's main loop
+ if necessary. Next screen from the stack is then displayed.
+
+ :param scr: if an UIScreen instance is passed it is checked to be the screen we are trying to close.
+ :type scr: UIScreen instance
+ """
+
oldscr, oldattr, oldloop = self._screens.pop()
if scr is not None:
assert oldscr == scr
+ # this cannot happen, if we are closing the window,
+ # the loop must have been running or not be there at all
+ assert oldloop != True
+
# we are in modal window, end it's loop
- assert oldloop != True # this cannot happen, if we are closing the window, the loop must be running or not there
if oldloop == False:
raise ExitMainLoop()
@@ -80,19 +162,32 @@ class App(object):
def _do_redraw(self):
"""Draws the current screen and returns True if user input is requested.
- If modal screen is requested, starts a new loop and initiates redraw after it ends."""
+ If modal screen is requested, starts a new loop and initiates redraw after it ends.
+
+ :return: this method returns True if user input processing is requested
+ :rtype: bool
+ """
+
+ # there is nothing to display, exit
if not self._screens:
raise ExitMainLoop()
+ # get the screen from the top of the stack
screen, args, newloop = self._screens[-1]
+ # new mainloop is requested
if newloop == True:
+ # change the record to indicate mainloop is running
self._screens.pop()
self._screens.append((screen, args, False))
+ # start the mainloop
self.mainloop()
+ # after the mainloop ends, set the redraw flag
+ # and skip the input processing once, to redisplay the screen first
self.redraw()
- input_needed = False # we have to skip input once, to redisplay the screen first
+ input_needed = False
else:
+ # get the widget tree from the screen and show it in the screen
input_needed = screen.refresh(args)
screen.window.show_all()
self._redraw = False
@@ -100,57 +195,99 @@ class App(object):
return input_needed
def run(self):
+ """This methods starts the application. Do not use self.mainloop() directly
+ as run() handles all the required exceptions needed to keep nested mainloops
+ working."""
+
try:
self.mainloop()
except ExitAllMainLoops:
pass
def mainloop(self):
+ """Single mainloop. Do not use directly, start the application using run()."""
+
+ # ask for redraw by default
self._redraw = True
+
+ # inital state
last_screen = None
error_counter = 0
+
+ # run until there is nothing else to display
while self._screens:
+ # if redraw is needed, separate the content on the screen from the
+ # stuff we are about to display now
if self._redraw:
print self._spacer
try:
+ # draw the screen if redraw is needed or the screen changed
+ # (unlikely to happen separately, but just be sure)
if self._redraw or last_screen != self._screens[-1]:
+ # we have fresh screen, reset error counter
+ error_counter = 0
if not self._do_redraw():
+ # if no input processing is requested, go for another cycle
continue
last_screen = self._screens[-1][0]
+ # get the screen's prompt
prompt = last_screen.prompt()
+
+ # None means prompt handled the input by itself
+ # ask for redraw and continue
if prompt is None:
self.redraw()
continue
+ # get the input from user
c = raw_input(prompt)
+ # process the input, if it wasn't processed (valid)
+ # increment the error counter
if not self.input(c):
error_counter += 1
+ # redraw the screen after 5 bad inputs
if error_counter >= 5:
self.redraw()
- if self._redraw:
- error_counter = 0
+ # end just this loop
except ExitMainLoop:
break
+
+ # propagate higher to end all loops
+ # not really needed here, but we might need
+ # more processing in the future
except ExitAllMainLoops:
raise
def input(self, key):
- """Method called to process unhandled input key presses."""
+ """Method called internally to process unhandled input key presses.
+ Also handles the main quit and close commands.
+
+ :param key: the string entered by user
+ :type key: unicode
+
+ :return: True if key was processed, False if it was not recognized
+ :rtype: True|False
+
+ """
+
+ # delegate the handling to active screen first
if self._screens:
key = self._screens[-1][0].input(key)
if key is None:
return True
+ # global close command
if self._screens and (key == 'c'):
self.close_screen()
return True
+ # global quit command
elif self._screens and (key == 'q'):
if self.quit_question:
d = self.quit_question(self, u"Do you really want to quit?")
@@ -162,6 +299,7 @@ class App(object):
return False
def redraw(self):
+ """Set the redraw flag so the screen is refreshed as soon as possible."""
self._redraw = True
@property
@@ -169,29 +307,51 @@ class App(object):
return self._header
@property
- def store(self):
- return self._store
-
- @property
def width(self):
+ """Return the total width of screen space we have available."""
return self._width
class UIScreen(object):
+ """Base class representing one TUI Screen. Shares some API with anaconda's GUI
+ to make it easy for devs to create similar UI with the familiar API."""
+
+ # title line of the screen
title = u"Screen.."
def __init__(self, app):
+ """
+ :param app: reference to application main class
+ :type app: instance of class App
+ """
+
self._app = app
+
+ # list that holds the content to be printed out
self._window = []
def refresh(self, args = None):
- """Method which prepares the screen to self._window. If user input is requested, return True."""
+ """Method which prepares the content desired on the screen to self._window.
+
+ :param args: optional argument passed from switch_screen calls
+ :type args: anything
+
+ :return: has to return True if input processing is requested, otherwise
+ the screen will get printed and the main loop will continue
+ :rtype: True|False
+ """
+
self._window = [self.title, u""]
+ return True
@property
def window(self):
+ """Return reference to the window instance. In TUI, just return self."""
return self
def show_all(self):
+ """Prepares all elements of self._window for output and then prints
+ them on the screen."""
+
for w in self._window:
if hasattr(w, "render"):
w.render(self.app.width)
@@ -200,32 +360,49 @@ class UIScreen(object):
show = show_all
def hide(self):
+ """This does nothing in TUI, it is here to make API similar."""
pass
def input(self, key):
- """Method called to process input. If the input is not handled here, return it."""
+ """Method called to process input. If the input is not handled here, return it.
+
+ :param key: input string to process
+ :type key: unicode
+
+ :return: return True or None if key was handled, False if the screen should not
+ process input on the App and key if you want it to.
+ :rtype: True|False|None|unicode
+ """
+
return key
def prompt(self):
- """Return the text to be shown as prompt or handle the prompt and return None."""
+ """Return the text to be shown as prompt or handle the prompt and return None.
+
+ :return: returns text to be shown next to the prompt for input or None
+ to skip further input processing
+ :rtype: unicode|None
+"""
return u"\tPlease make your choice from above ['q' to quit]: "
@property
def app(self):
+ """The reference to this Screen's assigned App instance."""
return self._app
def close(self):
+ """Close the current screen."""
self.app.close_screen(self)
class Widget(object):
def __init__(self, max_width = None, default = None):
"""Initializes base Widgets buffer.
- @param max_width server as a hint about screen size to write method with default arguments
- @type max_width int
+ :param max_width: server as a hint about screen size to write method with default arguments
+ :type max_width: int
- @param default string containing the default content to fill the buffer with
- @type default string
+ :param default: string containing the default content to fill the buffer with
+ :type default: string
"""
self._buffer = []
@@ -236,10 +413,13 @@ class Widget(object):
@property
def height(self):
+ """The current height of the internal buffer."""
return len(self._buffer)
@property
def width(self):
+ """The current width of the internal buffer
+ (id of the first empty column)."""
return reduce(lambda acc,l: max(acc, len(l)), self._buffer, 0)
def clear(self):
@@ -255,8 +435,8 @@ class Widget(object):
def render(self, width = None):
"""This method has to redraw the widget's self._buffer.
- @param width the width of buffer requested by the caller
- @type width int
+ :param width: the width of buffer requested by the caller
+ :type width: int
This method will commonly call render of child widgets and then draw and write
methods to copy their contents to self._buffer
@@ -264,10 +444,18 @@ class Widget(object):
self.clear()
def __unicode__(self):
+ """Method to render the screen when printing as unicode string."""
return u"\n".join([u"".join(l) for l in self._buffer])
def setxy(self, row, col):
- """Sets cursor position."""
+ """Sets cursor position.
+
+ :param row: row id, starts with 0 at the top of the screen
+ :type row: int
+
+ :param col: column id, starts with 0 on the left side of the screen
+ :type col: int
+ """
self._cursor = (row, col)
@property
@@ -281,32 +469,33 @@ class Widget(object):
def draw(self, w, row = None, col = None, block = False):
"""This method copies w widget's content to this widget's buffer at row, col position.
- @param w widget to take content from
- @type w class Widget
+ :param w: widget to take content from
+ :type w: class Widget
- @param row row number to start at (default is at the cursor position)
- @type row int
+ :param row: row number to start at (default is at the cursor position)
+ :type row: int
- @param col column number to start at (default is at the cursor position)
- @type col int
+ :param col: column number to start at (default is at the cursor position)
+ :type col: int
- @param block when printing newline, start at column col (True) or at column 0 (False)
- @type boolean
+ :param block: when printing newline, start at column col (True) or at column 0 (False)
+ :type block: boolean
"""
-
+ # if the starting row is not present, start at the cursor position
if row is None:
row = self._cursor[0]
+ # if the starting column is not present, start at the cursor position
if col is None:
col = self._cursor[1]
- # fill up rows
+ # fill up rows to accomodate for w.height
if self.height < row + w.height:
for i in range(row + w.height - self.height):
self._buffer.append(list())
- # append columns
+ # append columns to accomodate for w.width
for l in range(row, row + w.height):
l_len = len(self._buffer[l])
w_len = len(w.content[l - row])
@@ -314,6 +503,7 @@ class Widget(object):
self._buffer[l] += ((col + w_len - l_len) * list(u" "))
self._buffer[l][col:col + w_len] = w.content[l - row][:]
+ # move the cursor to new spot
if block:
self._cursor = (row + w.height, col)
else:
@@ -322,20 +512,20 @@ class Widget(object):
def write(self, text, row = None, col = None, width = None, block = False):
"""This method emulates typing machine writing to this widget's buffer.
- @param text text to type
- @type text unicode
+ :param text: text to type
+ :type text: unicode
- @param row row number to start at (default is at the cursor position)
- @type row int
+ :param row: row number to start at (default is at the cursor position)
+ :type row: int
- @param col column number to start at (default is at the cursor position)
- @type col int
+ :param col: column number to start at (default is at the cursor position)
+ :type col: int
- @param width wrap at "col" + "width" column (default is at self._max_width)
- @type width int
+ :param width: wrap at "col" + "width" column (default is at self._max_width)
+ :type width: int
- @param block when printing newline, start at column col (True) or at column 0 (False)
- @type boolean
+ :param block: when printing newline, start at column col (True) or at column 0 (False)
+ :type block: boolean
"""
if row is None:
@@ -384,12 +574,12 @@ class Widget(object):
self._cursor = (x, y)
-class HelloWorld(UIScreen):
- def show(self, args = None):
- print """Hello World\nquit by typing 'quit'"""
- return True
-
if __name__ == "__main__":
+ class HelloWorld(UIScreen):
+ def show(self, args = None):
+ print """Hello World\nquit by typing 'quit'"""
+ return True
+
a = App("Hello World")
s = HelloWorld(a, None)
a.schedule_screen(s)
diff --git a/pyanaconda/ui/tui/simpleline/widgets.py b/pyanaconda/ui/tui/simpleline/widgets.py
index 8dde043..d6ef5c6 100644
--- a/pyanaconda/ui/tui/simpleline/widgets.py
+++ b/pyanaconda/ui/tui/simpleline/widgets.py
@@ -26,20 +26,49 @@ __all__ = ["TextWidget", "ColumnWidget", "CheckboxWidget", "CenterWidget"]
import base
class TextWidget(base.Widget):
+ """Class to handle wrapped text output."""
+
def __init__(self, text):
+ """
+ :param text: text to format
+ :type text: unicode
+ """
+
base.Widget.__init__(self)
self._text = text
def render(self, width):
+ """Renders the text widget limited to width number of columns
+ (wraps to the next line when the text is longer).
+
+ :param width: maximum width allocated to the string
+ :type width: int
+
+ :raises
+ """
+
base.Widget.render(self, width)
self.write(self._text, width = width)
class CenterWidget(base.Widget):
+ """Class to handle horizontal centering of content."""
+
def __init__(self, w):
+ """
+ :param w: widget to center
+ :type w: base.Widget
+ """
base.Widget.__init__(self)
self._w = w
def render(self, width):
+ """
+ Render the centered widget to internal buffer.
+
+ :param width: maximum width the widget should use
+ :type width: int
+ """
+
base.Widget.render(self, width)
self._w.render(width)
self.draw(self._w, col = (width - self._w.width) / 2)
@@ -48,11 +77,11 @@ class ColumnWidget(base.Widget):
def __init__(self, columns, spacing = 0):
"""Create text columns
- @param columns list containing (column width, [list of widgets to put into this column])
- @type columns [(int, [...]), ...]
+ :param columns: list containing (column width, [list of widgets to put into this column])
+ :type columns: [(int, [...]), ...]
- @param spacing number of spaces to use between columns
- @type int
+ :param spacing: number of spaces to use between columns
+ :type spacing: int
"""
base.Widget.__init__(self)
@@ -60,26 +89,60 @@ class ColumnWidget(base.Widget):
self._columns = columns
def render(self, width):
+ """Render the widget to it's internal buffer
+
+ :param width: the maximum width the widget can use
+ :type width: int
+
+ :return: nothing
+ :rtype: None
+ """
+
base.Widget.render(self, width)
+ # the lefmost empty column
x = 0
+
+ # iterate over tuples (column width, column content)
for col_width,col in self._columns:
+
+ # set cursor to first line and leftmost empty column
self.setxy(0, x)
+ # if requested width is None, limit the maximum to width
+ # and set minimum to 0
if col_width is None:
col_max_width = width - self.cursor[1]
col_width = 0
else:
col_max_width = col_width
+ # render and draw contents of column
for item in col:
item.render(col_max_width)
self.draw(item, block = True)
+ # recompute the leftmost empty column
x = max((x + col_width), self.width) + self._spacing
class CheckboxWidget(base.Widget):
- def __init__(self, key = None, title = None, text = None, completed = None):
+ """Widget to show checkbox with (un)checked box, name and description."""
+
+ def __init__(self, key = "x", title = None, text = None, completed = None):
+ """
+ :param key: tick character to be used inside [ ]
+ :type key: character
+
+ :param title: the title next to the [ ] box
+ :type title: unicode
+
+ :param text: the description text to be shown on the second row in ()
+ :type text: unicode
+
+ :param completed: is the checkbox ticked or not?
+ :type completed: True|False
+ """
+
base.Widget.__init__(self)
self._key = key
self._title = title
@@ -87,37 +150,49 @@ class CheckboxWidget(base.Widget):
self._completed = completed
def render(self, width):
+ """Render the widget to internal buffer. It should be max width
+ characters wide."""
base.Widget.render(self, width)
if self.completed:
- checkchar = "x"
+ checkchar = self._key
else:
checkchar = " "
+ # prepare the checkbox
checkbox = TextWidget("[%s]" % checkchar)
data = []
+ # append lines
if self.title:
data.append(TextWidget(self.title))
if self.text:
data.append(TextWidget("(%s)" % self.text))
+ # the checkbox has two columns
+ # [x] is one and is 3 chars wide
+ # text is second and can occupy width - 3 - 1 (for space) chars
cols = ColumnWidget([(3, [checkbox]), (width - 4, data)], 1)
cols.render(width)
+
+ # transfer the column widget rendered stuff to internal buffer
self.draw(cols)
@property
def title(self):
+ """Returns the first line (main title) of the checkbox."""
return self._title
@property
def completed(self):
+ """Returns the state of the checkbox, checked is True."""
return self._completed
@property
def text(self):
+ """Contains the description text from the second line."""
return self._text
if __name__ == "__main__":
--
1.7.10.4
More information about the anaconda-patches
mailing list