[PATCH 01/13] Text based UI framework core

Vratislav Podzimek vpodzime at redhat.com
Wed Aug 8 11:23:57 UTC 2012


On Wed, 2012-08-08 at 10:52 +0200, Martin Sivak wrote:
> ---
>  pyanaconda/ui/tui/simpleline/__init__.py |   23 ++
>  pyanaconda/ui/tui/simpleline/base.py     |  385 ++++++++++++++++++++++++++++++
>  pyanaconda/ui/tui/simpleline/widgets.py  |  138 +++++++++++
>  3 files changed, 546 insertions(+)
>  create mode 100644 pyanaconda/ui/tui/simpleline/__init__.py
>  create mode 100644 pyanaconda/ui/tui/simpleline/base.py
>  create mode 100644 pyanaconda/ui/tui/simpleline/widgets.py
> 
> diff --git a/pyanaconda/ui/tui/simpleline/__init__.py b/pyanaconda/ui/tui/simpleline/__init__.py
> new file mode 100644
> index 0000000..e290e4a
> --- /dev/null
> +++ b/pyanaconda/ui/tui/simpleline/__init__.py
> @@ -0,0 +1,23 @@
> +# Library containing the TUI framework for Anaconda installer.
> +#
> +# Copyright (C) 2012  Red Hat, Inc.
> +#
> +# This copyrighted material is made available to anyone wishing to use,
> +# modify, copy, or redistribute it subject to the terms and conditions of
> +# the GNU General Public License v.2, or (at your option) any later version.
> +# This program is distributed in the hope that it will be useful, but WITHOUT
> +# ANY WARRANTY expressed or implied, including the implied warranties of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
> +# Public License for more details.  You should have received a copy of the
> +# GNU General Public License along with this program; if not, write to the
> +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
> +# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
> +# source code or documentation are not subject to the GNU General Public
> +# License and may only be used or replicated with the express permission of
> +# Red Hat, Inc.
> +#
> +# Red Hat Author(s): Martin Sivak <msivak at redhat.com>
> +#
> +
> +from base import *
> +from widgets import *
> diff --git a/pyanaconda/ui/tui/simpleline/base.py b/pyanaconda/ui/tui/simpleline/base.py
> new file mode 100644
> index 0000000..7b1562a
> --- /dev/null
> +++ b/pyanaconda/ui/tui/simpleline/base.py
> @@ -0,0 +1,385 @@
> +# Base classes for the Anaconda TUI framework.
> +#
> +# Copyright (C) 2012  Red Hat, Inc.
> +#
> +# This copyrighted material is made available to anyone wishing to use,
> +# modify, copy, or redistribute it subject to the terms and conditions of
> +# the GNU General Public License v.2, or (at your option) any later version.
> +# This program is distributed in the hope that it will be useful, but WITHOUT
> +# ANY WARRANTY expressed or implied, including the implied warranties of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
> +# Public License for more details.  You should have received a copy of the
> +# GNU General Public License along with this program; if not, write to the
> +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
> +# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
> +# source code or documentation are not subject to the GNU General Public
> +# License and may only be used or replicated with the express permission of
> +# Red Hat, Inc.
> +#
> +# Red Hat Author(s): Martin Sivak <msivak at redhat.com>
> +#
> +
> +__all__ = ["ExitMainLoop", "App", "UIScreen", "Widget"]
> +
> +class ExitAllMainLoops(Exception):
> +    pass
> +
> +class ExitMainLoop(Exception):
> +    pass
> +
> +
> +class App(object):
> +    def __init__(self, title, yes_or_no_question = None):
> +        self._header = title
> +        self._spacer = "\n".join(2*[80*"="])
> +        self.quit_question = yes_or_no_question
> +
> +        # 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
> +        self._screens = []
> +
> +    def switch_screen(self, ui, args = None):
> +        """Schedules a screen to replace the current one."""
> +        oldscr, oldattr, oldloop = self._screens.pop()
> +        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."""
> +        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."""
> +        self._screens.append((ui, args, True))
> +        self.redraw()
> +
> +    def schedule_screen(self, ui, args = None):
> +        """Add screen to the bottom of the stack."""
> +        self._screens.insert(0, (ui, args, None))
> +
> +    def close_screen(self, scr = None):
> +        oldscr, oldattr, oldloop = self._screens.pop()
> +        if scr is not None:
> +            assert oldscr == scr
> +
> +        # 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()
> +
> +        if self._screens:
> +            self.redraw()
> +        else:
> +            raise ExitMainLoop()
> +
> +    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 not self._screens:
> +            raise ExitMainLoop()
> +
> +        screen, args, newloop = self._screens[-1]
> +        input_needed = screen.refresh(args)
> +        screen.window.show_all()
> +
> +        if newloop == True:
> +            self._screens.pop()
> +            self._screens.append((screen, args, False))
> +            self.mainloop()
> +            self.redraw()
> +            input_needed = False # we have to skip input once, to redisplay the screen first
> +
> +        self._redraw = False
> +
> +        return input_needed
> +
> +    def run(self):
> +        try:
> +            self.mainloop()
> +        except ExitAllMainLoops:
> +            pass
> +
> +    def mainloop(self):
> +        self._redraw = True
> +        last_screen = None
> +        error_counter = 0
> +        while self._screens:
> +            try:
> +                if self._redraw or last_screen != self._screens[-1]:
> +                    if not self._do_redraw():
> +                        continue
> +
> +                last_screen = self._screens[-1]
> +
> +                c = raw_input(last_screen.prompt())
> +                if not self.input(c):
> +                    error_counter += 1
> +
> +                if error_counter >= 5:
> +                    self.redraw()
> +
> +                if self._redraw:
> +                    error_counter = 0
> +                    print self._spacer
> +            except ExitMainLoop:
> +                break
> +            except ExitAllMainLoops:
> +                raise
> +
> +    def input(self, key):
> +        """Method called to process unhandled input key presses."""
> +        if self._screens:
> +            key = self._screens[-1][0].input(key)
> +            if key is None:
> +                return True
> +
> +        if self._screens and (key == 'c'):
> +            self.close_screen()
> +            return True
> +
> +        elif self._screens and (key == 'quit'):
Shouldn't (key == 'q') be here?

> +            if self.quit_question:
> +                self.switch_screen_modal(self.quit_question(u"Do you really want to quit?"))
> +                if self.quit_question.answer:
> +                    raise ExitAllMainLoops()
> +            return True
> +
> +        return False
> +
> +    def redraw(self):
> +        self._redraw = True
> +
> +    @property
> +    def header(self):
> +        return self._header
> +
> +    @property
> +    def store(self):
> +        return self._store
> +
> +
> +class UIScreen(object):
> +    title = u"Screen.."
> +
> +    def __init__(self, app, data):
> +        self._app = app
> +        self._data = data
> +        self._window = []
> +
> +    def refresh(self, args = None):
> +        """Method which prepares the screen to self._window. If user input is requested, return True."""
> +        self.window = [self.title, u""]
> +
> +    @property
> +    def window(self):
> +        return self
> +
> +    def show_all(self):
> +        for w in self._window:
> +            if hasattr(w, "render"):
> +                w.render(app.width)
> +            print unicode(w)
> +
> +    show = show_all
> +
> +    def hide(self):
> +        pass
> +
> +    def input(self, key):
> +        """Method called to process input. If the input is not handled here, return it."""
> +        return key
> +
> +    def prompt(self):
> +        return u"\tPlease make your choice from above ['q' to quit]: "
> +
> +    @property
> +    def data(self):
> +        return self._data
> +
> +    @property
> +    def app(self):
> +        return self._app
> +
> +    def close(self):
> +        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 default string containing the default content to fill the buffer with
> +           @type default string
> +           """
> +
> +        self._buffer = []
> +        if default:
> +            self._buffer = [[c for c in l] for l in default.split("\n")]
> +        self._max_width = max_width
> +        self._cursor = (0, 0) # row, col
> +
> +    @property
> +    def height(self):
> +        return len(self._buffer)
> +
> +    @property
> +    def width(self):
> +        return reduce(lambda acc,l: max(acc, len(l)), self._buffer, 0)
> +
> +    def clear(self):
> +        """Clears this widgets buffer and resets cursor."""
> +        self._buffer = list()
> +        self._cursor = (0, 0)
> +
> +    @property
> +    def content(self):
> +        """This has to return list (rows) of lists (columns) with one character elements."""
> +        return self._buffer
> +
> +    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
> +
> +           This method will commonly call render of child widgets and then draw and write
> +           methods to copy their contents to self._buffer
> +           """
> +        self.clear()
> +
> +    def __unicode__(self):
> +        return u"\n".join([u"".join(l) for l in self._buffer])
> +
> +    def setxy(self, row, col):
> +        """Sets cursor position."""
> +        self._cursor = (row, col)
> +
> +    @property
> +    def cursor(self):
> +        return self._cursor
> +
> +    def setend(self):
> +        """Sets the cursor to first column in new line at the end."""
> +        self._cursor = (self.height, 0)
> +
> +    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 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 block when printing newline, start at column col (True) or at column 0 (False)
> +           @type boolean
> +           """
> +
> +
> +        if row is None:
> +            row = self._cursor[0]
> +
> +        if col is None:
> +            col = self._cursor[1]
> +
> +        # fill up rows
> +        if self.height < row + w.height:
> +            for i in range(row + w.height - self.height):
> +                self._buffer.append(list())
> +
> +        # append columns
> +        for l in range(row, row + w.height):
> +            l_len = len(self._buffer[l])
> +            w_len = len(w.content[l - row])
> +            if l_len < col + w_len:
> +                self._buffer[l] += ((col + w_len - l_len) * list(u" "))
> +            self._buffer[l][col:col + w_len] = w.content[l - row][:]
> +
> +        if block:
> +            self._cursor = (row + w.height, col)
> +        else:
> +            self._cursor = (row + w.height, 0)
> +
> +    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 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 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
> +           """
> +
> +        if row is None:
> +            row = self._cursor[0]
> +
> +        if col is None:
> +            col = self._cursor[1]
> +
> +        if width is None:
> +            width = self._max_width - col
> +
> +        x = row
> +        y = col
> +
> +        # emulate typing machine
> +        for c in text:
> +            # if the line is not in buffer, create it
> +            if x >= len(self._buffer):
> +                for i in range(x - len(self._buffer) + 1):
> +                    self._buffer.append(list())
> +
> +            # if the line's length is not enough, fill it with spaces
> +            if y >= len(self._buffer[x]):
> +                self._buffer[x] += ((y - len(self._buffer[x]) + 1) * list(u" "))
> +
> +            # process newline
> +            if c == "\n":
> +                x += 1
> +                if block:
> +                    y = col
> +                else:
> +                    y = 0
> +                continue
> +
> +            # "type" character
> +            self._buffer[x][y] = c
> +
> +            # shift to the next char
> +            y += 1
> +            if y >= col + width:
> +                x += 1
> +                if block:
> +                    y = col
> +                else:
> +                    y = 0
> +
> +        self._cursor = (x, y)
> +
> +class HelloWorld(UIScreen):
> +    def show(self, args = None):
> +        print """Hello World\nquit by typing 'quit'"""
> +        return True
> +
> +if __name__ == "__main__":
> +    a = App("Hello World")
> +    s = HelloWorld(a, None)
> +    a.schedule_screen(s)
> +    a.run()
> diff --git a/pyanaconda/ui/tui/simpleline/widgets.py b/pyanaconda/ui/tui/simpleline/widgets.py
> new file mode 100644
> index 0000000..2d79e3f
> --- /dev/null
> +++ b/pyanaconda/ui/tui/simpleline/widgets.py
> @@ -0,0 +1,138 @@
> +# encoding: utf-8
> +#
> +# Widgets for Anaconda TUI.
> +#
> +# Copyright (C) 2012  Red Hat, Inc.
> +#
> +# This copyrighted material is made available to anyone wishing to use,
> +# modify, copy, or redistribute it subject to the terms and conditions of
> +# the GNU General Public License v.2, or (at your option) any later version.
> +# This program is distributed in the hope that it will be useful, but WITHOUT
> +# ANY WARRANTY expressed or implied, including the implied warranties of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
> +# Public License for more details.  You should have received a copy of the
> +# GNU General Public License along with this program; if not, write to the
> +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
> +# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
> +# source code or documentation are not subject to the GNU General Public
> +# License and may only be used or replicated with the express permission of
> +# Red Hat, Inc.
> +#
> +# Red Hat Author(s): Martin Sivak <msivak at redhat.com>
> +#
> +
> +__all__ = ["TextWidget", "ColumnWidget", "CheckboxWidget", "CenterWidget"]
> +
> +import base
> +
> +class TextWidget(base.Widget):
> +    def __init__(self, text):
> +        base.Widget.__init__(self)
> +        self._text = text
> +
> +    def render(self, width):
> +        self.clear()
> +        self.write(self._text, width = width)
> +
> +class CenterWidget(base.Widget):
> +    def __init__(self, w):
> +        base.Widget.__init__(self)
> +        self._w = w
> +
> +    def render(self, width):
> +        self.clear()
> +        self._w.render(width)
> +        self.draw(self._w, col = (width - self._w.width) / 2)
> +
> +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 spacing number of spaces to use between columns
> +           @type int
> +           """
> +
> +        base.Widget.__init__(self)
> +        self._spacing = spacing
> +        self._columns = columns
> +
> +    def render(self, width):
> +        self.clear()
> +
> +        x = 0
> +        for col_width,col in self._columns:
> +            self.setxy(0, x)
> +
> +            if col_width is None:
> +                col_max_width = width - self.cursor[1]
> +                col_width = 0
> +            else:
> +                col_max_width = col_width
> +
> +            for item in col:
> +                item.render(col_max_width)
> +                self.draw(item, block = True)
> +
> +            x = max((x + col_width), self.width) + self._spacing
> +
> +class CheckboxWidget(base.Widget):
> +    def __init__(self, key = None, title = None, text = None, completed = None):
> +        base.Widget.__init__(self)
> +        self._key = key
> +        self._title = title
> +        self._text = text
> +        self._completed = completed
> +
> +    def render(self, width):
> +        self.clear()
> +
> +        if self.completed:
> +            checkchar = "x"
> +        else:
> +            checkchar = " "
> +
> +        checkbox = TextWidget("[%s]" % checkchar)
> +
> +        data = []
> +
> +        if self.title:
> +            data.append(TextWidget(self.title))
> +
> +        if self.text:
> +            data.append(TextWidget("(%s)" % self.text))
> +
> +        cols = ColumnWidget([(3, [checkbox]), (width - 4, data)], 1)
> +        cols.render(width)
> +        self.draw(cols)
> +
> +    @property
> +    def title(self):
> +        return self._title
> +
> +    @property
> +    def completed(self):
> +        return self._completed
> +
> +    @property
> +    def text(self):
> +        return self._text
> +
> +if __name__ == "__main__":
> +    t1 = TextWidget(u"Můj krásný dlouhý text")
> +    t2 = TextWidget(u"Test")
> +    t3 = TextWidget(u"Test 2")
> +    t4 = TextWidget(u"Krásný dlouhý text podruhé")
> +    t5 = TextWidget(u"Test 3")
> +
> +    c = ColumnWidget([(15, [t1, t2, t3]), (10, [t4, t5])], spacing = 1)
> +    c.render(80)
> +    print unicode(c)
> +
> +    print 80*"-"
> +
> +    c = ColumnWidget([(20, [t1, t2, t3]), (25, [t4, t5]), (15, [t1, t2, t3])], spacing = 3)
> +    c.render(80)
> +    print unicode(c)


-- 
Vratislav Podzimek

Anaconda Rider | Red Hat, Inc. | Brno - Czech Republic



More information about the anaconda-patches mailing list