[PATCH 01/13] Text based UI framework core

Martin Sivak msivak at redhat.com
Wed Aug 8 08:52:39 UTC 2012


---
 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'):
+            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)
-- 
1.7.10.4



More information about the anaconda-patches mailing list