diff options
author | Martin Sivak <msivak@redhat.com> | 2012-08-02 13:34:38 +0200 |
---|---|---|
committer | Martin Sivak <msivak@redhat.com> | 2012-08-06 13:32:21 +0200 |
commit | a2a9cc907ab6f81d7b6dac6ec2ac00c5ab8f4eae (patch) | |
tree | 4ca98a479a34fe472321f58b35db4e90f718ce81 /pyanaconda | |
parent | 7f96916bc05a1ed2ca4c1df8855762a3e6e4fd2f (diff) | |
download | anaconda-a2a9cc907ab6f81d7b6dac6ec2ac00c5ab8f4eae.tar.gz anaconda-a2a9cc907ab6f81d7b6dac6ec2ac00c5ab8f4eae.tar.xz anaconda-a2a9cc907ab6f81d7b6dac6ec2ac00c5ab8f4eae.zip |
Add documentation to the simpleline library for TUI
Diffstat (limited to 'pyanaconda')
-rw-r--r-- | pyanaconda/ui/tui/simpleline/base.py | 294 | ||||
-rw-r--r-- | 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 573f28b23..0c9f750d1 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@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 8dde0433b..d6ef5c633 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__": |