[node-patches] Change in ovirt-node[master]: Introduce abstract UI Elements

fabiand at fedoraproject.org fabiand at fedoraproject.org
Tue Dec 11 20:09:34 UTC 2012


Fabian Deutsch has uploaded a new change for review.

Change subject: Introduce abstract UI Elements
......................................................................

Introduce abstract UI Elements

Change-Id: I53fe904095f99397b96dcaefa50e3dd40cb33805
Signed-off-by: Fabian Deutsch <fabiand at fedoraproject.org>
---
A scripts/tui/src/.pylintrc
M scripts/tui/src/Makefile
M scripts/tui/src/app
A scripts/tui/src/ovirt/node/exceptions.py
M scripts/tui/src/ovirt/node/plugins/__init__.py
M scripts/tui/src/ovirt/node/plugins/example.py
M scripts/tui/src/ovirt/node/plugins/features.py
M scripts/tui/src/ovirt/node/plugins/kdump.py
M scripts/tui/src/ovirt/node/plugins/ping.py
M scripts/tui/src/ovirt/node/plugins/status.py
M scripts/tui/src/ovirt/node/plugins/usage.py
M scripts/tui/src/ovirt/node/tui.py
A scripts/tui/src/ovirt/node/ui/__init__.py
A scripts/tui/src/ovirt/node/ui/builder.py
R scripts/tui/src/ovirt/node/ui/widgets.py
M scripts/tui/src/ovirt/node/utils/process.py
M scripts/tui/src/ovirt/node/valid.py
17 files changed, 854 insertions(+), 419 deletions(-)


  git pull ssh://gerrit.ovirt.org:29418/ovirt-node refs/changes/83/9883/1

diff --git a/scripts/tui/src/.pylintrc b/scripts/tui/src/.pylintrc
new file mode 100644
index 0000000..bfafc62
--- /dev/null
+++ b/scripts/tui/src/.pylintrc
@@ -0,0 +1,249 @@
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Profiled execution.
+profile=no
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+
+[MESSAGES CONTROL]
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time.
+#enable=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once).
+#disable=
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html
+output-format=text
+
+# Include message's id in output
+include-ids=no
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells whether to display a full report or only the messages
+reports=yes
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Add a comment according to your evaluation note. This is used by the global
+# evaluation report (RP0004).
+comment=no
+
+
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set).
+ignored-classes=SQLObject
+
+# When zope mode is activated, add a predefined set of Zope acquired attributes
+# to generated-members.
+zope=no
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E0201 when accessed. Python regular
+# expressions are accepted.
+generated-members=REQUEST,acl_users,aq_parent
+
+
+[BASIC]
+
+# Required attributes for module, separated by a comma
+required-attributes=
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=map,filter,apply,input
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression which should only match correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression which should only match correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct method names
+method-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct instance attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,ex,Run,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Regular expression which should only match functions or classes name which do
+# not require a docstring
+no-docstring-rgx=__.*__
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string='    '
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the beginning of the name of dummy variables
+# (i.e. not used).
+dummy-variables-rgx=_|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+[CLASSES]
+
+# List of interface methods to ignore, separated by a comma. This is used for
+# instance to not check methods defines in Zope's Interface base class.
+ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+max-branchs=12
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception
diff --git a/scripts/tui/src/Makefile b/scripts/tui/src/Makefile
index 17262af..54a930c 100644
--- a/scripts/tui/src/Makefile
+++ b/scripts/tui/src/Makefile
@@ -3,7 +3,7 @@
 XMLSOURCES=$(shell find . -name \*.xml -or -name \*.xsl)
 CLEANFILES=$(shell find . -name \*.pyc -o -name \*.pyo -o -name \*~)
 
-check-local: doctests pep8 pyflakes pylint
+check-local: doctests pep8 pyflakes
 	@echo -e "---\n Passed.\n---"
 
 doctests:
diff --git a/scripts/tui/src/app b/scripts/tui/src/app
index 1ea2e40..844a59a 100755
--- a/scripts/tui/src/app
+++ b/scripts/tui/src/app
@@ -1,3 +1,5 @@
 #!/bin/bash
 
-PYTHONPATH=$PYTHONPATH:. python -B -m ovirt.node.main "$@"
+export PYTHONPATH=$PYTHONPATH:
+export PYTHONDONTWRITEBYTECODE=x
+python -B -m ovirt.node.main "$@"
diff --git a/scripts/tui/src/ovirt/node/exceptions.py b/scripts/tui/src/ovirt/node/exceptions.py
new file mode 100644
index 0000000..ad31931
--- /dev/null
+++ b/scripts/tui/src/ovirt/node/exceptions.py
@@ -0,0 +1,44 @@
+#!/usr/bin/python
+#
+# __init__.py - Copyright (C) 2012 Red Hat, Inc.
+# Written by Fabian Deutsch <fabiand at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty 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.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+"""
+oVirt Node specific exceptions
+"""
+
+
+class InvalidData(Exception):
+    """E.g. if a string contains characters which are not allowed
+    """
+    def __init__(self, msg):
+        self.message = msg
+
+    def __str__(self):
+        return repr(self.message)
+
+
+class Concern(InvalidData):
+    """E.g. if a password is not secure enough
+    FIXME very ... unspecific
+    """
+    def __init__(self, msg):
+        self.message = msg
+
+    def __str__(self):
+        return repr(self.message)
diff --git a/scripts/tui/src/ovirt/node/plugins/__init__.py b/scripts/tui/src/ovirt/node/plugins/__init__.py
index 6c6f541..2560fdc 100644
--- a/scripts/tui/src/ovirt/node/plugins/__init__.py
+++ b/scripts/tui/src/ovirt/node/plugins/__init__.py
@@ -25,6 +25,7 @@
 import logging
 
 import ovirt.node.plugins
+import ovirt.node.exceptions
 
 LOGGER = logging.getLogger(__name__)
 
@@ -123,7 +124,7 @@
                 msg = self.validators()[path](value)
                 # True and None are allowed values
                 if msg not in [True, None]:
-                    raise ovirt.node.plugins.InvalidData(msg)
+                    raise ovirt.node.exceptions.InvalidData(msg)
         return True
 
     def has_ui(self):
@@ -144,17 +145,6 @@
             List of (path, widget)
         """
         raise NotImplementedError()
-
-    def ui_config(self):
-        """Specifies additional details for the UI
-        E.g. if some defaults should be omitted (default save button).
-
-        save_button: If the save button shall be displayed (True)
-
-        Returns:
-            A dict of config items and their values.
-        """
-        return {}
 
     def on_change(self, changes):
         """Applies the changes to the plugins model, will do all required logic
@@ -186,7 +176,7 @@
             self.on_change(model)
         except NotImplementedError:
             LOGGER.debug("Plugin has no model")
-        except InvalidData:
+        except ovirt.node.exceptions.InvalidData:
             LOGGER.warning("Plugin has invalid model")
             is_valid = False
         return is_valid
@@ -234,190 +224,3 @@
         else:
             LOGGER.debug("No changes detected")
         return self.on_merge(real_changes)
-
-
-# http://stackoverflow.com/questions/739654/understanding-python-decorators
-class Widget(object):
-    _signal_cbs = None
-
-    def __init__(self):
-        """Registers all widget signals.
-        All signals must be given in self.signals
-        """
-        LOGGER.debug("Initializing new %s" % self)
-
-    @staticmethod
-    def signal_change(func):
-        """A decorator for methods which should emit signals
-        """
-        def wrapper(self, userdata=None, *args, **kwargs):
-            signame = func.__name__
-            self._register_signal(signame)
-            self.emit_signal(signame, userdata)
-            return func(self, userdata)
-        return wrapper
-
-    def _register_signal(self, name):
-        """Each signal that get's emitted must be registered using this
-        function.
-
-        This is just to have an overview over the signals.
-        """
-        if self._signal_cbs is None:
-            self._signal_cbs = {}
-        if name not in self._signal_cbs:
-            self._signal_cbs[name] = []
-            LOGGER.debug("Registered new signal '%s' for '%s'" % (name, self))
-
-    def connect_signal(self, name, cb):
-        """Connect an callback to a signal
-        """
-        if not self._signal_cbs:
-            raise Exception("Signals not initialized %s for %s" % (name, self))
-        if name not in self._signal_cbs:
-            raise Exception("Unregistered signal %s for %s" % (name, self))
-        self._signal_cbs[name].append(cb)
-
-    def emit_signal(self, name, userdata=None):
-        """Emit a signal
-        """
-        if self._signal_cbs is None or name not in self._signal_cbs:
-            return False
-        for cb in self._signal_cbs[name]:
-            LOGGER.debug("CB for sig %s: %s" % (name, cb))
-            cb(self, userdata)
-
-    def set_text(self, value):
-        """A general way to set the "text" of a widget
-        """
-        raise NotImplementedError
-
-
-class InputWidget(Widget):
-    """
-    """
-
-    def __init__(self, is_enabled):
-        self.enabled(is_enabled)
-        super(InputWidget, self).__init__()
-
-    @Widget.signal_change
-    def enabled(self, is_enabled=None):
-        if is_enabled in [True, False]:
-            self._enabled = is_enabled
-        return self._enabled
-
-    @Widget.signal_change
-    def text(self, text=None):
-        if text != None:
-            self._text = text
-        return self._text
-
-    def set_text(self, txt):
-        self.text(txt)
-
-
-class Label(Widget):
-    """Represents a r/o label
-    """
-
-    def __init__(self, text):
-        self.text(text)
-        super(Label, self).__init__()
-
-    @Widget.signal_change
-    def text(self, text=None):
-        if text != None:
-            self._text = text
-        return self._text
-
-    def set_text(self, txt):
-        self.text(txt)
-
-
-class Header(Label):
-    pass
-
-
-class KeywordLabel(Label):
-    """A label consisting of a prominent keyword and a value.
-    E.g.: <b>Networking:</b> Enabled
-    """
-
-    def __init__(self, keyword, text=""):
-        super(Label, self).__init__()
-        self.keyword = keyword
-        self.text(text)
-
-
-class Entry(InputWidget):
-    """Represents an entry field
-    TODO multiline
-    """
-
-    def __init__(self, label, value=None, enabled=True):
-        self.label = label
-        self._text = value
-        super(Entry, self).__init__(enabled)
-
-
-class PasswordEntry(Entry):
-    pass
-
-
-class Button(Label):
-    def __init__(self, label, enabled=True):
-        Label.__init__(self, label)
-#        InputWidget.__init__(self, enabled)
-
-
-class SaveButton(Button):
-    def __init__(self, enabled=True):
-        super(SaveButton, self).__init__(self, "Save", enabled)
-
-
-class Divider(Widget):
-    def __init__(self, char=u" "):
-        self.char = char
-
-
-class Options(Widget):
-
-    def __init__(self, label, options):
-        self.label = label
-        self.options = options
-        self.option(options[0])
-        super(Options, self).__init__()
-
-    @Widget.signal_change
-    def option(self, option=None):
-        if option in self.options:
-            self._option = option
-        return self._option
-
-    def set_text(self, txt):
-        self.option(txt)
-
-
-class InvalidData(Exception):
-    """E.g. if a string contains characters which are not allowed
-    """
-    def __init__(self, msg):
-        self.message = msg
-
-    def __str__(self):
-        return repr(self.message)
-
-
-class Concern(InvalidData):
-    """E.g. if a password is not secure enough
-    """
-    def __init__(self, msg):
-        self.message = msg
-
-    def __str__(self):
-        return repr(self.message)
-
-
-class ContentRefreshRequest(Exception):
-    pass
diff --git a/scripts/tui/src/ovirt/node/plugins/example.py b/scripts/tui/src/ovirt/node/plugins/example.py
index e46df24..aacbe86 100644
--- a/scripts/tui/src/ovirt/node/plugins/example.py
+++ b/scripts/tui/src/ovirt/node/plugins/example.py
@@ -24,6 +24,8 @@
 import logging
 
 import ovirt.node.plugins
+import ovirt.node.ui
+import ovirt.node.exceptions
 import ovirt.node.valid
 
 LOGGER = logging.getLogger(__name__)
@@ -63,16 +65,17 @@
         """
         widgets = [
             ("foo.section",
-                ovirt.node.plugins.Header("Subsection")),
+                ovirt.node.ui.Header("Subsection")),
             ("foo.hostname",
-                ovirt.node.plugins.Entry(label="Hostname")),
+                ovirt.node.ui.Entry(label="Hostname")),
             ("foo.port",
-                ovirt.node.plugins.Entry(label="Port")),
+                ovirt.node.ui.Entry(label="Port")),
             ("foo.password",
-                ovirt.node.plugins.PasswordEntry(label="Password")),
+                ovirt.node.ui.PasswordEntry(label="Password")),
         ]
         self._widgets = dict(widgets)
-        return widgets
+        page = ovirt.node.ui.Page(widgets)
+        return page
 
     def on_change(self, changes):
         """Applies the changes to the plugins model, will do all required logic
@@ -82,10 +85,11 @@
             LOGGER.debug("Found foo.hostname")
 
             if "/" in changes["foo.hostname"]:
-                raise ovirt.node.plugins.InvalidData("No slash allowed")
+                raise ovirt.node.exceptions.InvalidData("No slash allowed")
 
             if len(changes["foo.hostname"]) < 5:
-                raise ovirt.node.plugins.Concern("Should be at least 5 chars")
+                raise ovirt.node.exceptions.Concern(
+                                                "Should be at least 5 chars")
 
             self._model.update(changes)
 
@@ -101,7 +105,7 @@
             LOGGER.debug("Found foo.port")
 
             if "/" in changes["foo.port"]:
-                raise ovirt.node.plugins.InvalidData("No slashes allowed")
+                raise ovirt.node.exceptions.InvalidData("No slashes allowed")
 
         return True
 
@@ -111,3 +115,7 @@
         LOGGER.debug("saving %s" % effective_changes)
         # Look for conflicts etc
         self._model.update(effective_changes)
+
+        page = ovirt.node.ui.Dialog("Saved!",
+                [("foo.text", ovirt.node.ui.Label("Saved"))])
+        return page
diff --git a/scripts/tui/src/ovirt/node/plugins/features.py b/scripts/tui/src/ovirt/node/plugins/features.py
index 20e8379..69cfc2b 100644
--- a/scripts/tui/src/ovirt/node/plugins/features.py
+++ b/scripts/tui/src/ovirt/node/plugins/features.py
@@ -24,6 +24,7 @@
 import logging
 
 import ovirt.node.plugins
+import ovirt.node.ui
 
 
 LOGGER = logging.getLogger(__name__)
@@ -44,11 +45,9 @@
 
     def ui_content(self):
         widgets = [
-            ("features.info", ovirt.node.plugins.Label(features))
+            ("features.info", ovirt.node.ui.Label(features))
         ]
-        return widgets
 
-    def ui_config(self):
-        return {
-            "save_button": False
-        }
+        page = ovirt.node.ui.Page(widgets)
+        page.has_save_button = False
+        return page
diff --git a/scripts/tui/src/ovirt/node/plugins/kdump.py b/scripts/tui/src/ovirt/node/plugins/kdump.py
index 219050f..88ca080 100644
--- a/scripts/tui/src/ovirt/node/plugins/kdump.py
+++ b/scripts/tui/src/ovirt/node/plugins/kdump.py
@@ -25,7 +25,7 @@
 
 import ovirt.node.plugins
 import ovirt.node.valid
-import ovirt.node.plugins
+import ovirt.node.ui
 import ovirt.node.utils
 
 LOGGER = logging.getLogger(__name__)
@@ -75,14 +75,16 @@
         This is an ordered list of (path, widget) tuples.
         """
         widgets = [
-            ("kdump.header", ovirt.node.plugins.Header("Configure Kdump")),
-            ("kdump.type", ovirt.node.plugins.Options("Type", self._types)),
-            ("kdump.ssh_location", ovirt.node.plugins.Entry("SSH Location")),
-            ("kdump.nfs_location", ovirt.node.plugins.Entry("NFS Location")),
+            ("kdump.header", ovirt.node.ui.Header("Configure Kdump")),
+            ("kdump.type", ovirt.node.ui.Options("Type", self._types)),
+            ("kdump.ssh_location", ovirt.node.ui.Entry("SSH Location")),
+            ("kdump.nfs_location", ovirt.node.ui.Entry("NFS Location")),
         ]
         # Save it "locally" as a dict, for better accessability
         self._widgets = dict(widgets)
-        return widgets
+
+        page = ovirt.node.ui.Page(widgets)
+        return page
 
     def on_change(self, changes):
         """Applies the changes to the plugins model, will do all required logic
diff --git a/scripts/tui/src/ovirt/node/plugins/ping.py b/scripts/tui/src/ovirt/node/plugins/ping.py
index ff24354..e3a3bb7 100644
--- a/scripts/tui/src/ovirt/node/plugins/ping.py
+++ b/scripts/tui/src/ovirt/node/plugins/ping.py
@@ -25,7 +25,7 @@
 
 import ovirt.node.plugins
 import ovirt.node.valid
-import ovirt.node.plugins
+import ovirt.node.ui
 import ovirt.node.utils.process
 
 LOGGER = logging.getLogger(__name__)
@@ -69,21 +69,19 @@
         This is an ordered list of (path, widget) tuples.
         """
         widgets = [
-            ("ping.header", ovirt.node.plugins.Header("Ping a remote host")),
-            ("ping.address", ovirt.node.plugins.Entry("Address")),
-            ("ping.count", ovirt.node.plugins.Entry("Count")),
-            ("ping.do_ping", ovirt.node.plugins.Button("Ping")),
-            ("ping.result-divider", ovirt.node.plugins.Divider("-")),
-            ("ping.result", ovirt.node.plugins.Label("Result:")),
+            ("ping.header", ovirt.node.ui.Header("Ping a remote host")),
+            ("ping.address", ovirt.node.ui.Entry("Address")),
+            ("ping.count", ovirt.node.ui.Entry("Count")),
+            ("ping.do_ping", ovirt.node.ui.Button("Ping")),
+            ("ping.result-divider", ovirt.node.ui.Divider("-")),
+            ("ping.result", ovirt.node.ui.Label("Result:")),
         ]
         # Save it "locally" as a dict, for better accessability
         self._widgets = dict(widgets)
-        return widgets
 
-    def ui_config(self):
-        return {
-            "save_button": False
-        }
+        page = ovirt.node.ui.Page(widgets)
+        page.has_save_button = False
+        return page
 
     def on_change(self, changes):
         """Applies the changes to the plugins model, will do all required logic
diff --git a/scripts/tui/src/ovirt/node/plugins/status.py b/scripts/tui/src/ovirt/node/plugins/status.py
index e334ad8..82bb6bb 100644
--- a/scripts/tui/src/ovirt/node/plugins/status.py
+++ b/scripts/tui/src/ovirt/node/plugins/status.py
@@ -25,13 +25,18 @@
 
 import ovirt.node.plugins
 import ovirt.node.valid
-import ovirt.node.plugins
+import ovirt.node.ui
 import ovirt.node.utils
 
 LOGGER = logging.getLogger(__name__)
 
 
 class Plugin(ovirt.node.plugins.NodePlugin):
+    """This is the summary page, summarizing all sorts of informations
+
+    There are no validators, as there is no input.
+    """
+
     _model = None
     _widgets = None
 
@@ -50,28 +55,21 @@
             }
         return self._model
 
-    def validators(self):
-        """Validators validate the input on change and give UI feedback
-        """
-        return {}
-
-    def ui_config(self):
-        return {
-            "save_button": False
-        }
-
     def ui_content(self):
         """Describes the UI this plugin requires
         This is an ordered list of (path, widget) tuples.
         """
         widgets = [
             ("status.networking",
-                ovirt.node.plugins.KeywordLabel("Networking")),
+                ovirt.node.ui.KeywordLabel("Networking")),
             ("status.logs",
-                ovirt.node.plugins.KeywordLabel("Logs")),
+                ovirt.node.ui.KeywordLabel("Logs")),
             ("status.vms.running",
-                ovirt.node.plugins.KeywordLabel("Running VMs")),
+                ovirt.node.ui.KeywordLabel("Running VMs")),
         ]
         # Save it "locally" as a dict, for better accessability
         self._widgets = dict(widgets)
-        return widgets
+
+        page = ovirt.node.ui.Page(widgets)
+        page.has_save_button = False
+        return page
diff --git a/scripts/tui/src/ovirt/node/plugins/usage.py b/scripts/tui/src/ovirt/node/plugins/usage.py
index 42080e2..8d0254c 100644
--- a/scripts/tui/src/ovirt/node/plugins/usage.py
+++ b/scripts/tui/src/ovirt/node/plugins/usage.py
@@ -24,6 +24,7 @@
 import logging
 
 import ovirt.node.plugins
+import ovirt.node.ui
 
 
 LOGGER = logging.getLogger(__name__)
@@ -46,11 +47,9 @@
 
     def ui_content(self):
         widgets = [
-            ("usage.info", ovirt.node.plugins.Label(usage))
+            ("usage.info", ovirt.node.ui.Label(usage))
         ]
-        return widgets
 
-    def ui_config(self):
-        return {
-            "save_button": False
-        }
+        page = ovirt.node.ui.Page(widgets)
+        page.has_save_button = False
+        return page
diff --git a/scripts/tui/src/ovirt/node/tui.py b/scripts/tui/src/ovirt/node/tui.py
index 76a092d..54c24c3 100644
--- a/scripts/tui/src/ovirt/node/tui.py
+++ b/scripts/tui/src/ovirt/node/tui.py
@@ -21,13 +21,17 @@
 """
 The urwid TUI base library
 """
+
 import urwid
 
 import logging
 
 import ovirt.node
-import ovirt.node.widgets
 import ovirt.node.plugins
+import ovirt.node.ui
+import ovirt.node.ui.widgets
+import ovirt.node.ui.builder
+import ovirt.node.exceptions
 import ovirt.node.utils
 
 LOGGER = logging.getLogger(__name__)
@@ -70,10 +74,10 @@
         self.app = app
 
     def __build_menu(self):
-        self.__menu = ovirt.node.widgets.PluginMenu(self.__pages)
+        self.__menu = ovirt.node.ui.widgets.PluginMenu(self.__pages)
 
         def menu_item_changed(plugin):
-            self.__change_to_page(plugin)
+            self.__change_to_plugin(plugin)
         urwid.connect_signal(self.__menu, 'changed', menu_item_changed)
 
     def __create_screen(self):
@@ -87,159 +91,36 @@
         footer = urwid.Text(self.footer, wrap='clip')
         return urwid.Frame(body, header, footer)
 
-    def __build_widget_for_item(self, plugin, path, item):
-        item_to_widget_map = {
-            ovirt.node.plugins.Label: ovirt.node.widgets.Label,
-            ovirt.node.plugins.Header: ovirt.node.widgets.Header,
-            ovirt.node.plugins.Entry: ovirt.node.widgets.Entry,
-            ovirt.node.plugins.PasswordEntry: ovirt.node.widgets.PasswordEntry,
-            ovirt.node.plugins.Button: ovirt.node.widgets.Button,
-            ovirt.node.plugins.SaveButton: ovirt.node.widgets.Button,
-            ovirt.node.plugins.Divider: ovirt.node.widgets.Divider,
-            ovirt.node.plugins.Options: ovirt.node.widgets.Options,
-            ovirt.node.plugins.KeywordLabel: ovirt.node.widgets.KeywordLabel,
-        }
+    def __change_to_plugin(self, plugin):
+        page = ovirt.node.ui.builder.page_from_plugin(self, plugin)
+        self._display_page(page)
 
-        assert type(item) in item_to_widget_map.keys(), \
-               "No widget for item type"
-
-        widget = None
-        widget_class = item_to_widget_map[type(item)]
-
-        if type(item) in [ovirt.node.plugins.Entry, \
-                          ovirt.node.plugins.PasswordEntry]:
-            widget = widget_class(item.label)
-
-            widget.enable(item.enabled)
-
-            def on_item_enabled_change_cb(w, v):
-                LOGGER.debug("Model changed, updating widget '%s': %s" % (w,
-                                                                          v))
-                if widget.selectable() != v:
-                    widget.enable(v)
-            item.connect_signal("enabled", on_item_enabled_change_cb)
-
-            def on_widget_value_change(widget, new_value):
-                LOGGER.debug("Widget changed, updating model '%s'" % path)
-
-                try:
-                    change = {path: new_value}
-                    plugin.validate(change)
-                    plugin._on_ui_change(change)
-                    widget.notice = ""
-                    widget.valid(True)
-                    plugin.__save_button.enable(True)
-
-                except ovirt.node.plugins.Concern as e:
-                    LOGGER.error("Concern when updating: %s" % e)
-
-                except ovirt.node.plugins.InvalidData as e:
-                    LOGGER.error("Invalid data when updating: %s" % e)
-                    widget.notice = e.message
-                    widget.valid(False)
-                    plugin.__save_button.enable(False)
-
-                # FIXME page validation must happen within tui, not plugin
-                # as UI data should be handled in tui
-
-                self.__loop.draw_screen()
-            urwid.connect_signal(widget, 'change', on_widget_value_change)
-
-        elif type(item) in [ovirt.node.plugins.Header, \
-                            ovirt.node.plugins.Label,
-                            ovirt.node.plugins.KeywordLabel]:
-            if type(item) is ovirt.node.plugins.KeywordLabel:
-                widget = widget_class(item.keyword, item.text())
-            else:
-                widget = widget_class(item.text())
-
-            def on_item_text_change_cb(w, v):
-                LOGGER.debug("Model changed, updating widget '%s': %s" % (w,
-                                                                          v))
-                widget.text(v)
-                self.__loop.draw_screen()
-            item.connect_signal("text", on_item_text_change_cb)
-
-        elif type(item) in [ovirt.node.plugins.Button,
-                            ovirt.node.plugins.SaveButton]:
-            widget = widget_class(item.text())
-
-            def on_widget_click_cb(widget, data=None):
-                if type(item) is ovirt.node.plugins.SaveButton:
-                    plugin._on_ui_save()
-                else:
-#                   Nit propagating the signal as a signal to the plugin
-#                   item.emit_signal("click", widget)
-                    plugin._on_ui_change({path: True})
-            urwid.connect_signal(widget, "click", on_widget_click_cb)
-
-        elif type(item) in [ovirt.node.plugins.Divider]:
-            widget = widget_class(item.char)
-
-        elif type(item) in [ovirt.node.plugins.Options]:
-            widget = widget_class(item.label, item.options,
-                                  plugin.model()[path])
-
-            def on_widget_change_cb(widget, data):
-                LOGGER.debug(data)
-                item.option(data)
-                plugin._on_ui_change({path: data})
-            urwid.connect_signal(widget, "change", on_widget_change_cb)
-
-        if type(item) in [ovirt.node.plugins.Entry,
-                          ovirt.node.plugins.PasswordEntry,
-                          ovirt.node.plugins.KeywordLabel,
-                          ovirt.node.plugins.Options]:
-            widget.set_text(plugin.model()[path])
-
-        return widget
-
-    def __build_plugin_widget(self, plugin):
-        """This method is building the widget for a plugin
-        """
-        widgets = []
-        config = {
-            "save_button": True
-        }
-
-        ui_content = plugin.ui_content()
-        config.update(plugin.ui_config())
-
-        # Always create the SaveButton, but only display it if requested
-        # FIXME introduce a widget for the plugin page
-        save = ovirt.node.widgets.Button("Save")
-        urwid.connect_signal(save, 'click', lambda x: plugin._on_ui_save())
-        plugin.__save_button = save
-
-        for path, item in ui_content:
-            widget = self.__build_widget_for_item(plugin, path, item)
-            widgets.append(("flow", widget))
-
-        if config["save_button"]:
-            widgets.append(urwid.Filler(save))
-
-        widgets.append(urwid.Filler(urwid.Text("")))
-
-        LOGGER.debug("Triggering initial sematic checks for '%s'" % plugin)
-        try:
-            plugin.check_semantics()
-        except:
-            self.notify("error", "Initial model validation failed.")
-
-        pile = urwid.Pile(widgets)
+    def _display_page(self, page):
         # FIXME why is this fixed?
-        widget = urwid.Filler(pile, ("fixed top", 1), height=20)
-        return widget
+        filler = urwid.Filler(page, ("fixed top", 1), height=20)
+        self.__page_frame.body = filler
 
-    def __change_to_page(self, plugin):
-        plugin_widget = self.__build_plugin_widget(plugin)
-        page = plugin_widget
-        self.__page_frame.body = page
+    def _display_dialog(self, body, title):
+        filler = urwid.Filler(body, ("fixed top", 1), height=20)
+        dialog = ovirt.node.ui.widgets.ModalDialog(filler,
+                                                   title, "esc",
+                                                   self.__loop.widget)
+        self.__loop.widget = dialog
+
+    def display(self, widget):
+        return {
+                   ovirt.node.ui.widgets.PageWidget: self._display_page
+               }[type(widget)](widget)
+
+    def popup(self, title, msg, buttons=None):
+        LOGGER.debug("Launching popup")
+        body = urwid.Filler(urwid.Text(msg))
+        self._display_dialog(body)
 
     def __filter_hotkeys(self, keys, raw):
         key = str(keys)
         LOGGER.debug("Keypress: %s" % key)
-        if type(self.__loop.widget) is ovirt.node.widgets.ModalDialog:
+        if type(self.__loop.widget) is ovirt.node.ui.widgets.ModalDialog:
             LOGGER.debug("Modal dialog escape: %s" % key)
             dialog = self.__loop.widget
             if dialog.escape_key in keys:
@@ -255,17 +136,17 @@
         self.register_hotkey(["esc"], self.quit)
         self.register_hotkey(["q"], self.quit)
 
+    def draw_screen(self):
+        self.__loop.draw_screen()
+
+    def watch_pipe(self, cb):
+        """Return a fd to be used as stdout, cb called for each line
+        """
+        return self.__loop.watch_pipe(cb)
+
     def notify(self, category, msg):
         LOGGER.info("UI notification (%s): %s" % (category, msg))
         # FIXME do notification
-
-    def popup(self, title, msg, buttons=None):
-        LOGGER.debug("Launching popup")
-
-        dialog = ovirt.node.widgets.ModalDialog(urwid.Filler(urwid.Text(msg)),
-                                                title, "esc",
-                                                self.__loop.widget)
-        self.__loop.widget = dialog
 
     def suspended(self):
         """Supspends the screen to do something in the foreground
diff --git a/scripts/tui/src/ovirt/node/ui/__init__.py b/scripts/tui/src/ovirt/node/ui/__init__.py
new file mode 100644
index 0000000..5141734
--- /dev/null
+++ b/scripts/tui/src/ovirt/node/ui/__init__.py
@@ -0,0 +1,227 @@
+#!/usr/bin/python
+#
+# __init__.py - Copyright (C) 2012 Red Hat, Inc.
+# Written by Fabian Deutsch <fabiand at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty 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.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+"""
+This contains abstract UI Elements
+"""
+import logging
+
+LOGGER = logging.getLogger(__name__)
+
+
+# http://stackoverflow.com/questions/739654/understanding-python-decorators
+class Element(object):
+    """An abstract UI Element.
+    This basically provides signals to communicate between real UI widget and
+    the plugins
+    """
+    _signal_cbs = None
+
+    def __init__(self):
+        """Registers all widget signals.
+        All signals must be given in self.signals
+        """
+        LOGGER.debug("Initializing new %s" % self)
+
+    @staticmethod
+    def signal_change(func):
+        """A decorator for methods which should emit signals
+        """
+        def wrapper(self, userdata=None, *args, **kwargs):
+            signame = func.__name__
+            self._register_signal(signame)
+            self.emit_signal(signame, userdata)
+            return func(self, userdata)
+        return wrapper
+
+    def _register_signal(self, name):
+        """Each signal that get's emitted must be registered using this
+        function.
+
+        This is just to have an overview over the signals.
+        """
+        if self._signal_cbs is None:
+            self._signal_cbs = {}
+        if name not in self._signal_cbs:
+            self._signal_cbs[name] = []
+            LOGGER.debug("Registered new signal '%s' for '%s'" % (name, self))
+
+    def connect_signal(self, name, cb):
+        """Connect an callback to a signal
+        """
+        if not self._signal_cbs:
+            raise Exception("Signals not initialized %s for %s" % (name, self))
+        if name not in self._signal_cbs:
+            raise Exception("Unregistered signal %s for %s" % (name, self))
+        self._signal_cbs[name].append(cb)
+
+    def emit_signal(self, name, userdata=None):
+        """Emit a signal
+        """
+        if self._signal_cbs is None or name not in self._signal_cbs:
+            return False
+        LOGGER.debug("Emitting '%s'" % name)
+        for cb in self._signal_cbs[name]:
+            LOGGER.debug("... %s" % cb)
+            cb(self, userdata)
+
+    def set_text(self, value):
+        """A general way to set the "text" of a widget
+        """
+        raise NotImplementedError
+
+
+class InputElement(Element):
+    """An abstract UI Element pfor user input
+    """
+
+    def __init__(self, is_enabled):
+        self.enabled(is_enabled)
+        super(InputElement, self).__init__()
+
+    @Element.signal_change
+    def enabled(self, is_enabled=None):
+        if is_enabled in [True, False]:
+            self._enabled = is_enabled
+        return self._enabled
+
+    @Element.signal_change
+    def text(self, text=None):
+        if text != None:
+            self._text = text
+        return self._text
+
+    def set_text(self, txt):
+        self.text(txt)
+
+
+class ContainerElement(Element):
+    """An abstract container Element containing other Elements
+    """
+    widgets = []
+    pass
+
+
+class Page(ContainerElement):
+    """An abstract page with a couple of widgets
+    """
+    has_save_button = True
+
+    def __init__(self, widgets):
+        self.widgets = widgets
+
+
+class Dialog(Page):
+    """An abstract dialog, similar to a page
+    """
+    def __init__(self, title, widgets):
+        self.title = title
+        super(Dialog, self).__init__(widgets)
+
+
+class Label(Element):
+    """Represents a r/o label
+    """
+
+    def __init__(self, text):
+        self.text(text)
+        super(Label, self).__init__()
+
+    @Element.signal_change
+    def text(self, text=None):
+        if text != None:
+            self._text = text
+        return self._text
+
+    def set_text(self, txt):
+        self.text(txt)
+
+
+class Header(Label):
+    pass
+
+
+class KeywordLabel(Label):
+    """A label consisting of a prominent keyword and a value.
+    E.g.: <b>Networking:</b> Enabled
+    """
+
+    def __init__(self, keyword, text=""):
+        super(Label, self).__init__()
+        self.keyword = keyword
+        self.text(text)
+
+
+class Entry(InputElement):
+    """Represents an entry field
+    TODO multiline
+    """
+
+    def __init__(self, label, value=None, enabled=True):
+        self.label = label
+        self._text = value
+        super(Entry, self).__init__(enabled)
+
+
+class PasswordEntry(Entry):
+    pass
+
+
+class Button(InputElement):
+    def __init__(self, label, enabled=True):
+        super(Button, self).__init__(enabled)
+        self.text(label)
+
+    @Element.signal_change
+    def label(self, label=None):
+        if label != None:
+            self._label = label
+        return self._label
+
+    def set_text(self, txt):
+        self.text(txt)
+
+
+class SaveButton(Button):
+    def __init__(self, enabled=True):
+        super(SaveButton, self).__init__("Save", enabled)
+
+
+class Divider(Element):
+    def __init__(self, char=u" "):
+        self.char = char
+
+
+class Options(Element):
+
+    def __init__(self, label, options):
+        self.label = label
+        self.options = options
+        self.option(options[0])
+        super(Options, self).__init__()
+
+    @Element.signal_change
+    def option(self, option=None):
+        if option in self.options:
+            self._option = option
+        return self._option
+
+    def set_text(self, txt):
+        self.option(txt)
diff --git a/scripts/tui/src/ovirt/node/ui/builder.py b/scripts/tui/src/ovirt/node/ui/builder.py
new file mode 100644
index 0000000..6b35c61
--- /dev/null
+++ b/scripts/tui/src/ovirt/node/ui/builder.py
@@ -0,0 +1,219 @@
+#!/usr/bin/python
+#
+# tui.py - Copyright (C) 2012 Red Hat, Inc.
+# Written by Fabian Deutsch <fabiand at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty 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.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+"""
+A visitor to build the urwid TUI from the abstract UI definitions.
+Is based on the visitor pattern
+"""
+
+import urwid
+
+import logging
+
+import ovirt.node
+import ovirt.node.plugins
+import ovirt.node.ui
+import ovirt.node.ui.widgets
+import ovirt.node.ui.builder
+import ovirt.node.exceptions
+import ovirt.node.utils
+
+LOGGER = logging.getLogger(__name__)
+
+
+def page_from_plugin(tui, plugin):
+    element = plugin.ui_content()
+    widget = None
+
+    # FIXME could also be done using dict.
+    if type(element) is ovirt.node.ui.Page:
+        widget = build_page(tui, plugin, element)
+    else:
+        raise Exception("Unknown element container: %s" % element)
+
+    return widget
+
+
+def build_page(tui, plugin, container):
+    widgets = []
+
+    # Always create the SaveButton, but only display it if requested
+    #save = ovirt.node.ui.widgets.Button("Save")
+    #urwid.connect_signal(save, 'click', lambda x: plugin._on_ui_save())
+    save = build_button("", ovirt.node.ui.SaveButton(), tui, plugin)
+    plugin._save_button = save
+
+    for path, item in container.widgets:
+        widget = widget_for_item(tui, plugin, path, item)
+        widgets.append(("flow", widget))
+
+    if container.has_save_button:
+        widgets.append(urwid.Filler(save))
+
+    widgets.append(urwid.Filler(urwid.Text("")))
+
+    LOGGER.debug("Triggering initial sematic checks for '%s'" % plugin)
+    try:
+        plugin.check_semantics()
+    except:
+        tui.notify("error", "Initial model validation failed.")
+
+    page = ovirt.node.ui.widgets.PageWidget(widgets)
+
+    return page
+
+
+def widget_for_item(tui, plugin, path, item):
+    item_to_builder = {
+        ovirt.node.ui.Label: build_label,
+        ovirt.node.ui.Header: build_label,
+        ovirt.node.ui.KeywordLabel: build_label,
+        ovirt.node.ui.Entry: build_entry,
+        ovirt.node.ui.PasswordEntry: build_entry,
+        ovirt.node.ui.Button: build_button,
+        ovirt.node.ui.SaveButton: build_button,
+        ovirt.node.ui.Divider: build_divider,
+        ovirt.node.ui.Options: build_options,
+    }
+
+    # Check if builder is available for UI Element
+    assert type(item) in item_to_builder.keys(), \
+           "No widget for item type"
+
+    # Build widget from UI Element
+    build_func = item_to_builder[type(item)]
+    widget = build_func(path, item, tui, plugin)
+
+    # Populate with values
+    if type(item) in [ovirt.node.ui.Entry,
+                      ovirt.node.ui.PasswordEntry,
+                      ovirt.node.ui.KeywordLabel,
+                      ovirt.node.ui.Options]:
+        widget.set_text(plugin.model()[path])
+
+    return widget
+
+
+def build_entry(path, item, tui, plugin):
+    widget = None
+    if type(item) is ovirt.node.ui.Entry:
+        widget = ovirt.node.ui.widgets.Entry(item.label)
+    else:
+        widget = ovirt.node.ui.widgets.PasswordEntry(item.label)
+
+    widget.enable(item.enabled)
+
+    def on_item_enabled_change_cb(w, v):
+        LOGGER.debug("Model changed, updating widget '%s': %s" % (w,
+                                                                  v))
+        if widget.selectable() != v:
+            widget.enable(v)
+    item.connect_signal("enabled", on_item_enabled_change_cb)
+
+    def on_widget_value_change(widget, new_value):
+        LOGGER.debug("Widget changed, updating model '%s'" % path)
+
+        try:
+            change = {path: new_value}
+            plugin.validate(change)
+            plugin._on_ui_change(change)
+            widget.notice = ""
+            widget.valid(True)
+            LOGGER.debug(plugin.__dict__)
+            plugin._save_button.enable(True)
+
+        except ovirt.node.exceptions.Concern as e:
+            LOGGER.error("Concern when updating: %s" % e)
+
+        except ovirt.node.exceptions.InvalidData as e:
+            LOGGER.error("Invalid data when updating: %s" % e)
+            widget.notice = e.message
+            widget.valid(False)
+            plugin._save_button.enable(False)
+
+        # FIXME page validation must happen within tui, not plugin
+        # as UI data should be handled in tui
+
+        tui.draw_screen()
+    urwid.connect_signal(widget, 'change', on_widget_value_change)
+
+    return widget
+
+
+def build_label(path, item, tui, plugin):
+    if type(item) is ovirt.node.ui.KeywordLabel:
+        widget = ovirt.node.ui.widgets.KeywordLabel(item.keyword,
+                                                    item.text())
+    elif type(item) is ovirt.node.ui.Header:
+        widget = ovirt.node.ui.widgets.Header(item.text())
+    else:
+        widget = ovirt.node.ui.widgets.Label(item.text())
+
+    def on_item_text_change_cb(w, v):
+        LOGGER.debug("Model changed, updating widget '%s': %s" % (w,
+                                                                  v))
+        widget.text(v)
+        # Redraw the screen if widget text is updated "outside" of the
+        # mainloop
+        tui.draw_screen()
+    item.connect_signal("text", on_item_text_change_cb)
+
+    return widget
+
+
+def build_button(path, item, tui, plugin):
+    widget = ovirt.node.ui.widgets.Button(item.text())
+
+    def on_widget_click_cb(widget, data=None):
+        LOGGER.debug("Button click: %s" % widget)
+        if type(item) is ovirt.node.ui.SaveButton:
+            r = plugin._on_ui_save()
+            LOGGER.debug("Got save: %s" % r)
+            # FIXME hacks to display page or dialog
+            if type(r) in [ovirt.node.ui.Page]:
+                w = build_page(tui, plugin, r)
+                tui.display(w)
+            elif type(r) in [ovirt.node.ui.Dialog]:
+                w = build_page(tui, plugin, r)
+                tui._display_dialog(w, r.title)
+        else:
+#           Not propagating the signal as a signal to the plugin
+#           item.emit_signal("click", widget)
+            plugin._on_ui_change({path: True})
+    urwid.connect_signal(widget, "click", on_widget_click_cb)
+
+    return widget
+
+
+def build_divider(path, item, tui, plugin):
+    return ovirt.node.ui.widgets.Divider(item.char)
+
+
+def build_options(path, item, tui, plugin):
+    widget = ovirt.node.ui.widgets.Options(item.label, item.options,
+                                           plugin.model()[path])
+
+    def on_widget_change_cb(widget, data):
+        LOGGER.debug(data)
+        item.option(data)
+        plugin._on_ui_change({path: data})
+    urwid.connect_signal(widget, "change", on_widget_change_cb)
+
+    return widget
diff --git a/scripts/tui/src/ovirt/node/widgets.py b/scripts/tui/src/ovirt/node/ui/widgets.py
similarity index 99%
rename from scripts/tui/src/ovirt/node/widgets.py
rename to scripts/tui/src/ovirt/node/ui/widgets.py
index 360f91b..35d2476 100644
--- a/scripts/tui/src/ovirt/node/widgets.py
+++ b/scripts/tui/src/ovirt/node/ui/widgets.py
@@ -351,3 +351,7 @@
             self.callback(self.choices[key])
         else:
             return key
+
+
+class PageWidget(urwid.Pile):
+    save_button = None
diff --git a/scripts/tui/src/ovirt/node/utils/process.py b/scripts/tui/src/ovirt/node/utils/process.py
index 87f54f1..b433ef8 100644
--- a/scripts/tui/src/ovirt/node/utils/process.py
+++ b/scripts/tui/src/ovirt/node/utils/process.py
@@ -28,7 +28,7 @@
 LOGGER = logging.getLogger(__name__)
 
 
-def popen_closefds(*args, **kwargs):
+def popen(*args, **kwargs):
     """subprocess.Popen wrapper to not leak file descriptors
 
     Args:
@@ -52,7 +52,7 @@
     Returns:
         retval of the process
     """
-    return popen_closefds(cmd, shell=True).wait()
+    return popen(cmd, shell=True).wait()
 
 
 def pipe(cmd, stdin=None):
@@ -67,8 +67,8 @@
     """
 
     LOGGER.debug("run '%s'" % cmd)
-    system_cmd = popen_closefds(cmd, shell=True, stdout=subprocess.PIPE,
-                                stderr=subprocess.PIPE)
+    system_cmd = popen(cmd, shell=True, stdout=subprocess.PIPE,
+                       stderr=subprocess.PIPE)
     stdout, stderr = system_cmd.communicate(stdin)
     if stdout:
         LOGGER.debug("out '%s'" % stdout)
@@ -88,9 +88,10 @@
     Yields:
         Lines read from stdout
     """
+    # https://github.com/wardi/urwid/blob/master/examples/subproc.py
     LOGGER.debug("run async '%s'" % cmd)
-    process = popen_closefds(cmd, shell=True, stdout=subprocess.PIPE,
-                                stderr=subprocess.PIPE, stdin=subprocess.PIPE)
+    process = popen(cmd, shell=True, stdout=subprocess.PIPE,
+                    stderr=subprocess.PIPE, stdin=stdin)
     if stdin:
         process.stdin.write(stdin)
     while process.poll() != 0:
diff --git a/scripts/tui/src/ovirt/node/valid.py b/scripts/tui/src/ovirt/node/valid.py
index 54da6fb..5953525 100644
--- a/scripts/tui/src/ovirt/node/valid.py
+++ b/scripts/tui/src/ovirt/node/valid.py
@@ -26,6 +26,7 @@
 import socket
 
 import ovirt.node.plugins
+import ovirt.node.exceptions
 
 
 logging.basicConfig(level=logging.DEBUG)
@@ -66,7 +67,7 @@
 
     def raise_exception(self):
         msg = self.__exception_msg.format(description=self.description)
-        raise ovirt.node.plugins.InvalidData(msg)
+        raise ovirt.node.exceptions.InvalidData(msg)
 
 
 class RegexValidator(Validator):


--
To view, visit http://gerrit.ovirt.org/9883
To unsubscribe, visit http://gerrit.ovirt.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I53fe904095f99397b96dcaefa50e3dd40cb33805
Gerrit-PatchSet: 1
Gerrit-Project: ovirt-node
Gerrit-Branch: master
Gerrit-Owner: Fabian Deutsch <fabiand at fedoraproject.org>



More information about the node-patches mailing list