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@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@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@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
On 07/24/2012 08:47 AM, Martin Sivak wrote:
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.
Well it certainly is a lot of code. Do we really want to maintain this much code, code that is different from the graphical UI? The more I think about this, the more I feel that text mode should be the barest minimum to get bits put on disk and a machine booting.
Maybe this much code is needed to get stuff on screen, maybe not.
Well,
we were asked to eventually have all features of GUI also in text mode. I would prefer having the necessary pieces there from the beginning than having to rewrite half of the code to accommodate the complexities.
Your examples used the Hub & Spoke model and you need a way of returning back to the Hub when the Spoke is done. We also need linear progression (to move between hubs or inside Spoke screens) and two "dialogs" (showError and showYesNoQuestion as required by UserInterface base class).
You also used columns in your screenshots, you can do that using hand crafted prints, but I do not think that is something I would like to maintain (formatted output can break it easily) in every screen, especially with the stars and [ ] markers. The formatting will add to the complexity when you try moving stuff around (not much, but still..) and we might have to focus on maintaining uniform look more.
This was an attempt to prepare a base classes, the UI code itself will be smaller thanks to it. If you drop the widgets code it is 166 lines including comments and docs that give you mainloop with uniform input (and possibly error) handling and screen switching.
Having this modular is some way is of course necessary, so it will either be a class or set of functions, because you will be repeating lots of code otherwise (= maintainers nightmare).
The widgets are obviously optional, but most of the code deals with the output limitations of dumb terminals. We can drop this part and use simple prints, but having a "widget" with similar API to the GUI hubs and spokes will probably help us refocus faster. Other teams will be probably writing some spokes too (ABRT, Satellite and some partners asked for it) so there has to be an API others can use to make independent screen development possible.
Those were the requirements I was thinking about while designing this and I admit I am using the App/UIScreen class pair in a project of mine already so I am a bit biased.
If you have a different ideas I am all ears.
Martin
----- Original Message -----
On 07/24/2012 08:47 AM, Martin Sivak wrote:
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.
Well it certainly is a lot of code. Do we really want to maintain this much code, code that is different from the graphical UI? The more I think about this, the more I feel that text mode should be the barest minimum to get bits put on disk and a machine booting.
Maybe this much code is needed to get stuff on screen, maybe not.
-- Jesse Keating Fedora -- Freedom² is a feature!
anaconda-patches mailing list anaconda-patches@lists.fedorahosted.org https://fedorahosted.org/mailman/listinfo/anaconda-patches
On 07/25/2012 02:47 AM, Martin Sivak wrote:
we were asked to eventually have all features of GUI also in text mode. I would prefer having the necessary pieces there from the beginning than having to rewrite half of the code to accommodate the complexities.
That's fair. I really don't know how many of those spokes we can actually do in text mode given the constraints though.
Your examples used the Hub & Spoke model and you need a way of returning back to the Hub when the Spoke is done. We also need linear progression (to move between hubs or inside Spoke screens) and two "dialogs" (showError and showYesNoQuestion as required by UserInterface base class).
This is true. My example was just about showing the UI, not about how to actually code it. As far as navigation is concerned, we certainly want to re-use as much of the existing ui code that's already in ui/gui/.
You also used columns in your screenshots, you can do that using hand crafted prints, but I do not think that is something I would like to maintain (formatted output can break it easily) in every screen, especially with the stars and [ ] markers. The formatting will add to the complexity when you try moving stuff around (not much, but still..) and we might have to focus on maintaining uniform look more.
I agree that columns are a bad idea. Particularly when you start throwing translated strings into those.
This was an attempt to prepare a base classes, the UI code itself will be smaller thanks to it. If you drop the widgets code it is 166 lines including comments and docs that give you mainloop with uniform input (and possibly error) handling and screen switching.
Having this modular is some way is of course necessary, so it will either be a class or set of functions, because you will be repeating lots of code otherwise (= maintainers nightmare).
This is true.
The widgets are obviously optional, but most of the code deals with the output limitations of dumb terminals. We can drop this part and use simple prints, but having a "widget" with similar API to the GUI hubs and spokes will probably help us refocus faster. Other teams will be probably writing some spokes too (ABRT, Satellite and some partners asked for it) so there has to be an API others can use to make independent screen development possible.
Agreed.
Those were the requirements I was thinking about while designing this and I admit I am using the App/UIScreen class pair in a project of mine already so I am a bit biased.
If you have a different ideas I am all ears.
I don't necessarily have something different in mind. I am struggling with how to make use of this code with a real spoke. Right now I'm looking at what it takes to re-use much of the code existing in ui/gui to setup a single text hub and single text spoke and somehow get it displayed. Your code may in fact become handy there, something in place of Gtk.main().
anaconda-patches@lists.fedorahosted.org