Hi,
this is an attempt to implement basic line only textual
UI framework for us to use.
It is condeptualy designed as stack of screens, where
the screen at the top is visible.
Individual screens can be implemented using the UIScreen parent
class and two methods: show and input
Active screens can (according to user input) be changed in four
ways:
- new screen is put onto the stack and the old one is
replaced (Next button equivalent)
- new screen is put onto the stack (Hub wants to show
Spoke with the possibility of direct return)
- new screen is displayed and the call returns to the
caller after it is closed (Dialog with data)
- screen is closed (and the next item from the stack is
displayed)
This means that the leaves (Spokes) do not have to know which
screen they should return back to.
There is no global dispatcher list in this architecture. If you
want to setup inital list of linear progression screens, use the
App.schedule_screen call.
I also included two "widgets" for us to structure the screen space
without using scrolling or curses. Text and Column. Widgets first
render to their internal buffers to determine size and the parent
then copies the "image" to it's own buffer space.
You can see the very simple examples of use at the ends of
individual files.
---
pyanaconda/ui/tui/simpleline/__init__.py | 23 ++
pyanaconda/ui/tui/simpleline/base.py | 337 ++++++++++++++++++++++++++++++
pyanaconda/ui/tui/simpleline/widgets.py | 78 +++++++
3 files changed, 438 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(a)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..acabb81
--- /dev/null
+++ b/pyanaconda/ui/tui/simpleline/base.py
@@ -0,0 +1,337 @@
+# 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(a)redhat.com>
+#
+
+__all__ = ["ExitMainLoop", "App", "UIScreen", "Widget"]
+
+class ExitMainLoop(Exception):
+ pass
+
+class App(object):
+ def __init__(self, title):
+ self._header = title
+ self._spacer = "\n".join(2*[80*"="])
+
+ # 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."""
+ self._screens.pop()
+ self._screens.append((ui, args, None))
+ 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, False))
+
+ 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.show(args)
+
+ if newloop == True:
+ self._screens.pop()
+ self._screens.append((screen, args, False))
+ self.run()
+ 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):
+ 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("\tPlease make your choice from above: ")
+ 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
+
+ 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 == 'quit'):
+ self.close_screen()
+ 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):
+ def __init__(self, app, data):
+ self._app = app
+ self._data = data
+
+ def show(self, args = None):
+ """Method which displays the screen. If user input is requested, return True."""
+ pass
+
+ def input(self, key):
+ """Method called to process input. If the input is not handled here, return it."""
+ return key
+
+ @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
+ """
+ pass
+
+ 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)
+
+ 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, col = self._cursor
+ elif 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, col = self._cursor
+ elif 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..0e134f1
--- /dev/null
+++ b/pyanaconda/ui/tui/simpleline/widgets.py
@@ -0,0 +1,78 @@
+# 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(a)redhat.com>
+#
+
+__all__ = ["TextWidget", "ColumnWidget"]
+
+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 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()
+
+ for col_width,col in self._columns:
+ for item in col:
+ item.render(col_width)
+ self.draw(item, block = True)
+
+ self.setxy(0, self.width + self._spacing)
+
+
+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