[PATCH 04/13] Create common abstract classes usable for all types of UI

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


---
 pyanaconda/ui/__init__.py                  |    6 +-
 pyanaconda/ui/common.py                    |  344 ++++++++++++++++++++++++++++
 pyanaconda/ui/gui/__init__.py              |   92 +-------
 pyanaconda/ui/gui/categories/__init__.py   |    3 +-
 pyanaconda/ui/gui/hubs/__init__.py         |   14 +-
 pyanaconda/ui/gui/spokes/__init__.py       |  222 +-----------------
 pyanaconda/ui/gui/spokes/custom.py         |   12 +-
 pyanaconda/ui/gui/spokes/datetime_spoke.py |    6 +-
 pyanaconda/ui/gui/spokes/keyboard.py       |    7 +-
 pyanaconda/ui/gui/spokes/lib/cart.py       |    4 +-
 pyanaconda/ui/gui/spokes/network.py        |    2 +-
 pyanaconda/ui/gui/spokes/source.py         |   18 +-
 pyanaconda/ui/gui/spokes/storage.py        |    4 +-
 pyanaconda/ui/tui/__init__.py              |   29 +++
 pyanaconda/ui/tui/common/__init__.py       |   26 ---
 pyanaconda/ui/tui/hubs/__init__.py         |    6 +-
 pyanaconda/ui/tui/simpleline/base.py       |    7 +-
 pyanaconda/ui/tui/spokes/__init__.py       |   40 +++-
 18 files changed, 467 insertions(+), 375 deletions(-)
 create mode 100644 pyanaconda/ui/common.py
 delete mode 100644 pyanaconda/ui/tui/common/__init__.py

diff --git a/pyanaconda/ui/__init__.py b/pyanaconda/ui/__init__.py
index 4fafd49..fcc6628 100644
--- a/pyanaconda/ui/__init__.py
+++ b/pyanaconda/ui/__init__.py
@@ -21,6 +21,10 @@
 
 __all__ = ["UserInterface", "collect"]
 
+import os
+import importlib
+import inspect
+
 class UserInterface(object):
     """This is the base class for all kinds of install UIs.  It primarily
        defines what kinds of dialogs and entry widgets every interface must
@@ -122,7 +126,7 @@ def collect(module_pattern, path, pred):
             continue
 
         mod_name = module_file[:-3]
-        module = importlib.import_module(module_pattern % mod_name))
+        module = importlib.import_module(module_pattern % mod_name)
 
         p = lambda obj: inspect.isclass(obj) and pred(obj)
 
diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py
new file mode 100644
index 0000000..03fa49d
--- /dev/null
+++ b/pyanaconda/ui/common.py
@@ -0,0 +1,344 @@
+class UIObject(object):
+    """This is the base class from which all other UI classes are derived.  It
+       thus contains only attributes and methods that are common to everything
+       else.  It should not be directly instantiated.
+       """
+
+    def __init__(self, data):
+        """Create a new UIObject instance, including loading its uiFile and
+           all UI-related objects.
+
+           Instance attributes:
+
+           data     -- An instance of a pykickstart Handler object.  The Hub
+                       never directly uses this instance.  Instead, it passes
+                       it down into Spokes when they are created and applied.
+                       The Hub simply stores this instance so it doesn't need
+                       to be passed by the user.
+        """
+        if self.__class__ is UIObject:
+            raise TypeError("UIObject is an abstract class")
+
+        self.skipTo = None
+        self._data = data
+
+    def initialize(self):
+        """Perform whatever actions are necessary to pre-fill the UI with
+           values.  This method is called only once, after the object is
+           created.  The difference between this method and __init__ is that
+           this method may take a long time (especially for NormalSpokes) and
+           thus may be run in its own thread.
+        """
+        pass
+
+    def retranslate(self):
+        """This method should be called when the current language is changed
+           in order to update the UI for the new language.  Since we don't get
+           any toolkit help for this, it is largely a manual process.
+        """
+        pass
+
+    def refresh(self):
+        """Perform whatever actions are necessary to reset the UI immediately
+           before it is displayed.  This method is called every time a screen
+           is shown, which could potentially be several times in the case of a
+           NormalSpoke.  Thus, it's important to not do things like populate
+           stores (which could result in the store having duplicate entries) or
+           anything that takes a long time (as that will result in a delay
+           between the user's action and showing the results).
+
+           For anything potentially long-lived, use the initialize method.
+        """
+        pass
+
+    @property
+    def showable(self):
+        """Should this object even be shown?  This method is useful for checking
+           some precondition before this screen is shown.  If False is returned,
+           the screen will be skipped and the object destroyed.
+        """
+        return True
+
+    def teardown(self):
+        """Perform whatever actions are necessary to clean up after this object
+           is done.  It's not necessary for every subclass to have an instance
+           of this method.
+
+           NOTE:  It is important for this method to not destroy self.window if
+           you are making a Spoke or Hub subclass.  It is assumed that once
+           these are instantiated, they live until the program terminates.  This
+           is required for various status notifications.
+        """
+        pass
+
+    @property
+    def window(self):
+        """Return an object with show_all and hide methods that is to be used
+           to display this UI object.
+        """
+        raise TypeError("UIObject.window has to be overriden")
+
+class Spoke(UIObject):
+    """A Spoke is a single configuration screen.  There are several different
+       places where a Spoke can be displayed, each of which will have its own
+       unique class.  A Spoke is typically used when an element in the Hub is
+       selected but can also be displayed before a Hub or between multiple
+       Hubs.
+
+       What amount of the UI layout a Spoke provides depends upon where it is
+       to be shown.  Regardless, the UI of a Spoke should be given by an
+       interface description file like glade as often as possible, though this
+       is not a strict requirement.
+
+       Class attributes:
+
+       category   -- Under which SpokeCategory shall this Spoke be displayed
+                     in the Hub?  This is a reference to a Hub subclass (not an
+                     object, but the class itself).  If no category is given,
+                     this Spoke will not be displayed.  Note that category is
+                     not required for any Spokes appearing before or after a
+                     Hub.
+       icon       -- The name of the icon to be displayed in the SpokeSelector
+                     widget corresponding to this Spoke instance.  If no icon
+                     is given, the default from SpokeSelector will be used.
+       title      -- The title to be displayed in the SpokeSelector widget
+                     corresponding to this Spoke instance.  If no title is
+                     given, the default from SpokeSelector will be used.
+    """
+    category = None
+    icon = None
+    title = None
+
+    def __init__(self, data, storage, payload, instclass):
+        """Create a new Spoke instance.
+
+           The arguments this base class accepts defines the API that spokes
+           have to work with.  A Spoke does not get free reign over everything
+           in the anaconda class, as that would be a big mess.  Instead, a
+           Spoke may count on the following:
+
+           ksdata       -- An instance of a pykickstart Handler object.  The
+                           Spoke uses this to populate its UI with defaults
+                           and to pass results back after it has run.
+           storage      -- An instance of storage.Storage.  This is useful for
+                           determining what storage devices are present and how
+                           they are configured.
+           payload      -- An instance of a packaging.Payload subclass.  This
+                           is useful for displaying and selecting packages to
+                           install, and in carrying out the actual installation.
+           instclass    -- An instance of a BaseInstallClass subclass.  This
+                           is useful for determining distribution-specific
+                           installation information like default package
+                           selections and default partitioning.
+        """
+        if self.__class__ is Spoke:
+            raise TypeError("Spoke is an abstract class")
+
+        UIObject.__init__(self, data)
+        self.storage = storage
+        self.payload = payload
+        self.instclass = instclass
+
+    def apply(self):
+        """Apply the selections made on this Spoke to the object's preset
+           data object.  This method must be provided by every subclass.
+        """
+        raise NotImplementedError
+
+    @property
+    def completed(self):
+        """Has this spoke been visited and completed?  If not, a special warning
+           icon will be shown on the Hub beside the spoke, and a highlighted
+           message will be shown at the bottom of the Hub.  Installation will not
+           be allowed to proceed until all spokes are complete.
+        """
+        return False
+
+    def execute(self):
+        """Cause the data object to take effect on the target system.  This will
+           usually be as simple as calling one or more of the execute methods on
+           the data object.  This method does not need to be provided by all
+           subclasses.
+
+           This method will be called in two different places:  (1) Immediately
+           after initialize on kickstart installs.  (2) Immediately after apply
+           in all cases.
+        """
+        pass
+
+    def initialize(self):
+        UIObject.initialize(self)
+
+    @property
+    def status(self):
+        """Given the current status of whatever this Spoke configures, return
+           a very brief string.  The purpose of this is to display something
+           on the Hub under the Spoke's title so the user can tell at a glance
+           how things are configured.
+
+           A spoke's status line on the Hub can also be overloaded to provide
+           information about why a Spoke is not yet ready, or if an error has
+           occurred when setting it up.  This can be done by calling
+           send_message from pyanaconda.ui.gui.communication with the target
+           Spoke's class name and the message to be displayed.
+
+           If the Spoke was not yet ready when send_message was called, the
+           message will be overwritten with the value of this status property
+           when the Spoke becomes ready.
+        """
+        raise NotImplementedError
+
+class NormalSpoke(Spoke):
+    @property
+    def indirect(self):
+        """If this property returns True, then this spoke is considered indirect.
+           An indirect spoke is one that can only be reached through another spoke
+           instead of directly through the hub.  One example of this is the
+           custom partitioning spoke, which may only be accessed through the
+           install destination spoke.
+
+           Indirect spokes do not need to provide a completed or status property.
+
+           For most spokes, overriding this property is unnecessary.
+        """
+        return False
+
+    @property
+    def ready(self):
+        """Returns True if the Spoke has all the information required to be
+           displayed.  Almost all spokes should keep the default value here.
+           Only override this method if the Spoke requires some potentially
+           long-lived process (like storage probing) before it's ready.
+
+           A Spoke may be marked as ready or not by calling send_ready or
+           send_not_ready from pyanaconda.ui.gui.communication with the
+           target Spoke's class name.
+
+           While a Spoke is not ready, a progress message may be shown to
+           give the user some feedback.  See the status property for details.
+        """
+        return True
+
+class StandaloneSpoke(NormalSpoke):
+    """A StandaloneSpoke is a Spoke subclass that is displayed apart from any
+       Hub.  It is suitable to be used as a Welcome screen.
+
+       From a layout perspective, a StandaloneSpoke provides a full screen
+       interface.  However, it also provides navigation information at the top
+       and bottom of the screen that makes it look like the StandaloneSpoke
+       fits into some other UI element.
+
+       Class attributes:
+
+       preForHub/postForHub   -- A reference to a Hub subclass this Spoke is
+                                 either a pre or post action for.  Only one of
+                                 these may be set at a time.  Note that all
+                                 post actions will be run for one hub before
+                                 any pre actions for the next.
+       priority               -- This value is used to sort pre and post
+                                 actions.  The lower a value, the earlier it
+                                 will be run.  So a value of 0 for a post action
+                                 ensures it will run immediately after a Hub,
+                                 while a value of 0 for a pre actions means
+                                 it will run as the first thing.
+    """
+    preForHub = None
+    postForHub = None
+
+    priority = 100
+
+    def __init__(self, data, storage, payload, instclass):
+        """Create a StandaloneSpoke instance."""
+        if self.__class__ is StandaloneSpoke:
+            raise TypeError("StandaloneSpoke is an abstract class")
+
+        if self.preForHub and self.postForHub:
+            raise AttributeError("StandaloneSpoke instance %s may not have both preForHub and postForHub set" % self)
+
+        Spoke.__init__(self, data, storage, payload, instclass)
+
+    """A NormalSpoke is a Spoke subclass that is displayed when the user
+       selects something on a Hub.  This is what most Spokes in anaconda will
+       be based on.
+
+       From a layout perspective, a NormalSpoke takes up the entire screen
+       therefore hiding the Hub and its action area.  The NormalSpoke also
+       provides some basic navigation information (where you are, what you're
+       installing, how to get back to the Hub) at the top of the screen.
+    """
+    def __init__(self, data, storage, payload, instclass):
+        """Create a NormalSpoke instance."""
+        if self.__class__ is NormalSpoke:
+            raise TypeError("NormalSpoke is an abstract class")
+
+        Spoke.__init__(self, data, storage, payload, instclass)
+        self.selector = None
+
+
+class PersonalizationSpoke(Spoke):
+    """A PersonalizationSpoke is a Spoke subclass that is displayed when the
+       user selects something on the Hub during package installation.
+
+       From a layout perspective, a PersonalizationSpoke takes up the middle
+       of the screen therefore hiding the Hub but leaving its action area
+       displayed.  This allows the user to continue seeing package installation
+       progress being made.  The PersonalizationSpoke also provides the same
+       basic navigation information at the top of the screen as a NormalSpoke.
+    """
+    def __init__(self, data, storage, payload, instclass):
+        """Create a PersonalizationSpoke instance."""
+        if self.__class__ is PersonalizationSpoke:
+            raise TypeError("PersonalizationSpoke is an abstract class")
+
+        Spoke.__init__(self, data, storage, payload, instclass)
+
+class Hub(UIObject):
+    """A Hub is an overview UI screen.  A Hub consists of one or more grids of
+       configuration options that the user may choose from.  Each grid is
+       provided by a SpokeCategory, and each option is provided by a Spoke.
+       When the user dives down into a Spoke and is finished interacting with
+       it, they are returned to the Hub.
+
+       Some Spokes are required.  The user must interact with all required
+       Spokes before they are allowed to proceed to the next stage of
+       installation.
+
+       From a layout perspective, a Hub is the entirety of the screen, though
+       the screen itself can be roughly divided into thirds.  The top third is
+       some basic navigation information (where you are, what you're
+       installing).  The middle third is the grid of Spokes.  The bottom third
+       is an action area providing additional buttons (quit, continue) or
+       progress information (during package installation).
+
+       Installation may consist of multiple chained Hubs, or Hubs with
+       additional standalone screens either before or after them.
+    """
+
+    def __init__(self, data, storage, payload, instclass):
+        """Create a new Hub instance.
+
+           The arguments this base class accepts defines the API that Hubs
+           have to work with.  A Hub does not get free reign over everything
+           in the anaconda class, as that would be a big mess.  Instead, a
+           Hub may count on the following:
+
+           ksdata       -- An instance of a pykickstart Handler object.  The
+                           Hub uses this to populate its UI with defaults
+                           and to pass results back after it has run.
+           storage      -- An instance of storage.Storage.  This is useful for
+                           determining what storage devices are present and how
+                           they are configured.
+           payload      -- An instance of a packaging.Payload subclass.  This
+                           is useful for displaying and selecting packages to
+                           install, and in carrying out the actual installation.
+           instclass    -- An instance of a BaseInstallClass subclass.  This
+                           is useful for determining distribution-specific
+                           installation information like default package
+                           selections and default partitioning.
+        """
+        UIObject.__init__(self, data)
+
+        self._spokes = {}
+        self.storage = storage
+        self.payload = payload
+        self.instclass = instclass
diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py
index 8cdc168..dbd0a28 100644
--- a/pyanaconda/ui/gui/__init__.py
+++ b/pyanaconda/ui/gui/__init__.py
@@ -20,7 +20,7 @@
 #
 import importlib, inspect, os, sys
 
-from pyanaconda.ui import UserInterface
+from pyanaconda.ui import UserInterface, common, collect
 from pyanaconda.ui.gui.utils import enlightbox
 
 import gettext
@@ -54,16 +54,7 @@ class GraphicalUserInterface(UserInterface):
 
         # First, grab a list of all the standalone spokes.
         path = os.path.join(os.path.dirname(__file__), "spokes")
-        standalones = collect("pyanaconda.ui.gui.spokes.%s", path, lambda obj: issubclass(obj, StandaloneSpoke) and \
-                              getattr(obj, "preForHub", False) or getattr(obj, "postForHub", False))
-
-        actionClasses = []
-        for hub in self._hubs:
-            actionClasses.extend(sorted(filter(lambda obj: getattr(obj, "preForHub", None) == hub, standalones),
-                                        key=lambda obj: obj.priority))
-            actionClasses.append(hub)
-            actionClasses.extend(sorted(filter(lambda obj: getattr(obj, "postForHub", None) == hub, standalones),
-                                        key=lambda obj: obj.priority))
+        actionClasses = self.getActionClasses("pyanaconda.ui.gui.spokes.%s", path, self._hubs, StandaloneSpoke)
 
         # Instantiate all hubs and their pre/post standalone spokes, passing
         # the arguments defining our spoke API and setting up continue/quit
@@ -185,8 +176,8 @@ class GraphicalUserInterface(UserInterface):
         if rc == 1:
             sys.exit(0)
 
-class UIObject(object):
-    """This is the base class from which all other UI classes are derived.  It
+class GUIObject(common.UIObject):
+    """This is the base class from which all other GUI classes are derived.  It
        thus contains only attributes and methods that are common to everything
        else.  It should not be directly instantiated.
 
@@ -241,8 +232,10 @@ class UIObject(object):
                        spoke off a hub.  They can only skip to the hub
                        itself.
         """
-        if self.__class__ is UIObject:
-            raise TypeError("UIObject is an abstract class")
+        common.UIObject.__init__(self, data)
+
+        if self.__class__ is GUIObject:
+            raise TypeError("GUIObject is an abstract class")
 
         # This couldn't possibly be a bigger hack job.  This structure holds the
         # untranslated strings out of each widget.  retranslate works by taking the
@@ -252,7 +245,6 @@ class UIObject(object):
         # original English, so we'd be looking up translations by translations.
         self._origStrings = {}
 
-        self.data = data
         self.skipTo = None
 
         from gi.repository import Gtk
@@ -300,15 +292,6 @@ class UIObject(object):
 
         _screenshotIndex += 1
 
-    def initialize(self):
-        """Perform whatever actions are necessary to pre-fill the UI with
-           values.  This method is called only once, after the object is
-           created.  The difference between this method and __init__ is that
-           this method may take a long time (especially for NormalSpokes) and
-           thus may be run in its own thread.
-        """
-        pass
-
     def retranslate(self):
         """This method should be called when the current language is changed
            in order to update the UI for the new language.  Since we don't get
@@ -342,39 +325,6 @@ class UIObject(object):
                 xlated = _(before)
                 getattr(obj, funcs[1])(xlated)
 
-    def refresh(self):
-        """Perform whatever actions are necessary to reset the UI immediately
-           before it is displayed.  This method is called every time a screen
-           is shown, which could potentially be several times in the case of a
-           NormalSpoke.  Thus, it's important to not do things like populate
-           stores (which could result in the store having duplicate entries) or
-           anything that takes a long time (as that will result in a delay
-           between the user's action and showing the results).
-
-           For anything potentially long-lived, use the initialize method.
-        """
-        pass
-
-    @property
-    def showable(self):
-        """Should this object even be shown?  This method is useful for checking
-           some precondition before this screen is shown.  If False is returned,
-           the screen will be skipped and the object destroyed.
-        """
-        return True
-
-    def teardown(self):
-        """Perform whatever actions are necessary to clean up after this object
-           is done.  It's not necessary for every subclass to have an instance
-           of this method.
-
-           NOTE:  It is important for this method to not destroy self.window if
-           you are making a Spoke or Hub subclass.  It is assumed that once
-           these are instantiated, they live until the program terminates.  This
-           is required for various status notifications.
-        """
-        pass
-
     @property
     def window(self):
         """Return the top-level object out of the GtkBuilder representation
@@ -388,7 +338,7 @@ class UIObject(object):
 
         return self._window
 
-class QuitDialog(UIObject):
+class QuitDialog(GUIObject):
     builderObjects = ["quitDialog"]
     mainWidgetName = "quitDialog"
     uiFile = "main.ui"
@@ -396,27 +346,3 @@ class QuitDialog(UIObject):
     def run(self):
         rc = self.window.run()
         return rc
-
-def collect(module_pattern, path, pred):
-    """Traverse the subdirectory (given by subpath) of this module's current
-       directory and find all classes that math the given category.  This is
-       then returned as a list of classes.  If category is None, this method
-       will return a list of all matching subclasses.
-
-       It is suggested you use collect_categories or collect_spokes instead of
-       this lower-level method.
-    """
-    retval = []
-    for module_file in os.listdir( + "/" + subpath):
-        if not module_file.endswith(".py") or module_file in [__file__, "__init__.py"]:
-            continue
-
-        mod_name = module_file[:-3]
-        module = importlib.import_module(module_pattern % mod_name))
-
-        p = lambda obj: inspect.isclass(obj) and pred(obj)
-
-        for (name, val) in inspect.getmembers(module, p):
-            retval.append(val)
-
-    return retval
diff --git a/pyanaconda/ui/gui/categories/__init__.py b/pyanaconda/ui/gui/categories/__init__.py
index 31739d7..1e43148 100644
--- a/pyanaconda/ui/gui/categories/__init__.py
+++ b/pyanaconda/ui/gui/categories/__init__.py
@@ -21,6 +21,7 @@
 
 N_ = lambda x: x
 
+import os.path
 from pyanaconda.ui.gui import collect
 
 __all__ = ["SpokeCategory", "collect_categories"]
@@ -72,4 +73,4 @@ class SpokeCategory(object):
 
 def collect_categories():
     """Return a list of all category subclasses."""
-    return collect("categories", lambda obj: getattr(obj, "displayOnHub", None) != None)
+    return collect("pyanaconda.ui.gui.categories.%s", os.path.dirname(__file__), lambda obj: getattr(obj, "displayOnHub", None) != None)
diff --git a/pyanaconda/ui/gui/hubs/__init__.py b/pyanaconda/ui/gui/hubs/__init__.py
index 0539dc0..8b8ee09 100644
--- a/pyanaconda/ui/gui/hubs/__init__.py
+++ b/pyanaconda/ui/gui/hubs/__init__.py
@@ -26,11 +26,12 @@ from gi.repository import GLib
 
 from pyanaconda.flags import flags
 
-from pyanaconda.ui.gui import UIObject
+from pyanaconda.ui import common
+from pyanaconda.ui.gui import GUIObject
 from pyanaconda.ui.gui.categories import collect_categories
 from pyanaconda.ui.gui.spokes import StandaloneSpoke, collect_spokes
 
-class Hub(UIObject):
+class Hub(GUIObject, common.Hub):
     """A Hub is an overview UI screen.  A Hub consists of one or more grids of
        configuration options that the user may choose from.  Each grid is
        provided by a SpokeCategory, and each option is provided by a Spoke.
@@ -74,7 +75,8 @@ class Hub(UIObject):
                            installation information like default package
                            selections and default partitioning.
         """
-        UIObject.__init__(self, data)
+        GUIObject.__init__(self, data)
+        common.Hub.__init__(self, data, storage, payload, instclass)
 
         self._autoContinue = False
         self._incompleteSpokes = []
@@ -82,10 +84,6 @@ class Hub(UIObject):
         self._notReadySpokes = []
         self._spokes = {}
 
-        self.storage = storage
-        self.payload = payload
-        self.instclass = instclass
-
     def _runSpoke(self, action):
         from gi.repository import Gtk
 
@@ -285,7 +283,7 @@ class Hub(UIObject):
         return True
 
     def refresh(self):
-        UIObject.refresh(self)
+        GUIObject.refresh(self)
         self._createBox()
 
         self._update_spoke_id = GLib.timeout_add_seconds(1, self._update_spokes)
diff --git a/pyanaconda/ui/gui/spokes/__init__.py b/pyanaconda/ui/gui/spokes/__init__.py
index 1fa3aa2..192022f 100644
--- a/pyanaconda/ui/gui/spokes/__init__.py
+++ b/pyanaconda/ui/gui/spokes/__init__.py
@@ -19,161 +19,24 @@
 # Red Hat Author(s): Chris Lumens <clumens at redhat.com>
 #
 
-from pyanaconda.ui.gui import UIObject, collect
+from pyanaconda.ui import collect, common
+from pyanaconda.ui.gui import GUIObject
+import os.path
 
 __all__ = ["Spoke", "StandaloneSpoke", "NormalSpoke", "PersonalizationSpoke",
            "collect_spokes"]
 
-class Spoke(UIObject):
-    """A Spoke is a single configuration screen.  There are several different
-       places where a Spoke can be displayed, each of which will have its own
-       unique class.  A Spoke is typically used when an element in the Hub is
-       selected but can also be displayed before a Hub or between multiple
-       Hubs.
-
-       What amount of the UI layout a Spoke provides depends upon where it is
-       to be shown.  Regardless, the UI of a Spoke should be given by an
-       interface description file like glade as often as possible, though this
-       is not a strict requirement.
-
-       Class attributes:
-
-       category   -- Under which SpokeCategory shall this Spoke be displayed
-                     in the Hub?  This is a reference to a Hub subclass (not an
-                     object, but the class itself).  If no category is given,
-                     this Spoke will not be displayed.  Note that category is
-                     not required for any Spokes appearing before or after a
-                     Hub.
-       icon       -- The name of the icon to be displayed in the SpokeSelector
-                     widget corresponding to this Spoke instance.  If no icon
-                     is given, the default from SpokeSelector will be used.
-       title      -- The title to be displayed in the SpokeSelector widget
-                     corresponding to this Spoke instance.  If no title is
-                     given, the default from SpokeSelector will be used.
-    """
-    category = None
-    icon = None
-    title = None
-
+class Spoke(GUIObject, common.Spoke):
     def __init__(self, data, storage, payload, instclass):
-        """Create a new Spoke instance.
-
-           The arguments this base class accepts defines the API that spokes
-           have to work with.  A Spoke does not get free reign over everything
-           in the anaconda class, as that would be a big mess.  Instead, a
-           Spoke may count on the following:
-
-           ksdata       -- An instance of a pykickstart Handler object.  The
-                           Spoke uses this to populate its UI with defaults
-                           and to pass results back after it has run.
-           storage      -- An instance of storage.Storage.  This is useful for
-                           determining what storage devices are present and how
-                           they are configured.
-           payload      -- An instance of a packaging.Payload subclass.  This
-                           is useful for displaying and selecting packages to
-                           install, and in carrying out the actual installation.
-           instclass    -- An instance of a BaseInstallClass subclass.  This
-                           is useful for determining distribution-specific
-                           installation information like default package
-                           selections and default partitioning.
-        """
-        if self.__class__ is Spoke:
-            raise TypeError("Spoke is an abstract class")
-
-        UIObject.__init__(self, data)
-        self.storage = storage
-        self.payload = payload
-        self.instclass = instclass
-
-    def apply(self):
-        """Apply the selections made on this Spoke to the object's preset
-           data object.  This method must be provided by every subclass.
-        """
-        raise NotImplementedError
-
-    @property
-    def completed(self):
-        """Has this spoke been visited and completed?  If not, a special warning
-           icon will be shown on the Hub beside the spoke, and a highlighted
-           message will be shown at the bottom of the Hub.  Installation will not
-           be allowed to proceed until all spokes are complete.
-        """
-        return False
-
-    def execute(self):
-        """Cause the data object to take effect on the target system.  This will
-           usually be as simple as calling one or more of the execute methods on
-           the data object.  This method does not need to be provided by all
-           subclasses.
-
-           This method will be called in two different places:  (1) Immediately
-           after initialize on kickstart installs.  (2) Immediately after apply
-           in all cases.
-        """
-        pass
+        GUIObject.__init__(self, data)
+        common.Spoke.__init__(self, data, storage, payload, instclass)
 
     def initialize(self):
-        UIObject.initialize(self)
+        GUIObject.initialize(self)
 
         self.window.set_property("window-name", self.title or "")
 
-    @property
-    def status(self):
-        """Given the current status of whatever this Spoke configures, return
-           a very brief string.  The purpose of this is to display something
-           on the Hub under the Spoke's title so the user can tell at a glance
-           how things are configured.
-
-           A spoke's status line on the Hub can also be overloaded to provide
-           information about why a Spoke is not yet ready, or if an error has
-           occurred when setting it up.  This can be done by calling
-           send_message from pyanaconda.ui.gui.communication with the target
-           Spoke's class name and the message to be displayed.
-
-           If the Spoke was not yet ready when send_message was called, the
-           message will be overwritten with the value of this status property
-           when the Spoke becomes ready.
-        """
-        raise NotImplementedError
-
-class StandaloneSpoke(Spoke):
-    """A StandaloneSpoke is a Spoke subclass that is displayed apart from any
-       Hub.  It is suitable to be used as a Welcome screen.
-
-       From a layout perspective, a StandaloneSpoke provides a full screen
-       interface.  However, it also provides navigation information at the top
-       and bottom of the screen that makes it look like the StandaloneSpoke
-       fits into some other UI element.
-
-       Class attributes:
-
-       preForHub/postForHub   -- A reference to a Hub subclass this Spoke is
-                                 either a pre or post action for.  Only one of
-                                 these may be set at a time.  Note that all
-                                 post actions will be run for one hub before
-                                 any pre actions for the next.
-       priority               -- This value is used to sort pre and post
-                                 actions.  The lower a value, the earlier it
-                                 will be run.  So a value of 0 for a post action
-                                 ensures it will run immediately after a Hub,
-                                 while a value of 0 for a pre actions means
-                                 it will run as the first thing.
-    """
-    preForHub = None
-    postForHub = None
-
-    priority = 100
-
-    def __init__(self, data, storage, payload, instclass):
-        """Create a StandaloneSpoke instance."""
-        if self.__class__ is StandaloneSpoke:
-            raise TypeError("StandaloneSpoke is an abstract class")
-
-        if self.preForHub and self.postForHub:
-            raise AttributeError("StandaloneSpoke instance %s may not have both preForHub and postForHub set" % self)
-
-        Spoke.__init__(self, data, storage, payload, instclass)
-
+class StandaloneSpoke(Spoke, common.StandaloneSpoke):
     def _on_continue_clicked(self, cb):
         self.apply()
         cb()
@@ -184,79 +47,18 @@ class StandaloneSpoke(Spoke):
         elif event == "quit":
             self.window.connect("quit-clicked", lambda *args: cb())
 
-class NormalSpoke(Spoke):
-    """A NormalSpoke is a Spoke subclass that is displayed when the user
-       selects something on a Hub.  This is what most Spokes in anaconda will
-       be based on.
-
-       From a layout perspective, a NormalSpoke takes up the entire screen
-       therefore hiding the Hub and its action area.  The NormalSpoke also
-       provides some basic navigation information (where you are, what you're
-       installing, how to get back to the Hub) at the top of the screen.
-    """
-    def __init__(self, data, storage, payload, instclass):
-        """Create a NormalSpoke instance."""
-        if self.__class__ is NormalSpoke:
-            raise TypeError("NormalSpoke is an abstract class")
-
-        Spoke.__init__(self, data, storage, payload, instclass)
-        self.selector = None
-
-    @property
-    def indirect(self):
-        """If this property returns True, then this spoke is considered indirect.
-           An indirect spoke is one that can only be reached through another spoke
-           instead of directly through the hub.  One example of this is the
-           custom partitioning spoke, which may only be accessed through the
-           install destination spoke.
-
-           Indirect spokes do not need to provide a completed or status property.
-
-           For most spokes, overriding this property is unnecessary.
-        """
-        return False
-
-    @property
-    def ready(self):
-        """Returns True if the Spoke has all the information required to be
-           displayed.  Almost all spokes should keep the default value here.
-           Only override this method if the Spoke requires some potentially
-           long-lived process (like storage probing) before it's ready.
-
-           A Spoke may be marked as ready or not by calling send_ready or
-           send_not_ready from pyanaconda.ui.gui.communication with the
-           target Spoke's class name.
-
-           While a Spoke is not ready, a progress message may be shown to
-           give the user some feedback.  See the status property for details.
-        """
-        return True
-
+class NormalSpoke(Spoke, common.NormalSpoke):
     def on_back_clicked(self, window):
         from gi.repository import Gtk
 
         self.window.hide()
         Gtk.main_quit()
 
-class PersonalizationSpoke(Spoke):
-    """A PersonalizationSpoke is a Spoke subclass that is displayed when the
-       user selects something on the Hub during package installation.
-
-       From a layout perspective, a PersonalizationSpoke takes up the middle
-       of the screen therefore hiding the Hub but leaving its action area
-       displayed.  This allows the user to continue seeing package installation
-       progress being made.  The PersonalizationSpoke also provides the same
-       basic navigation information at the top of the screen as a NormalSpoke.
-    """
-    def __init__(self, data, storage, payload, instclass):
-        """Create a PersonalizationSpoke instance."""
-        if self.__class__ is PersonalizationSpoke:
-            raise TypeError("PersonalizationSpoke is an abstract class")
-
-        Spoke.__init__(self, data, storage, payload, instclass)
+class PersonalizationSpoke(Spoke, common.PersonalizationSpoke):
+    pass
 
 def collect_spokes(category):
     """Return a list of all spoke subclasses that should appear for a given
        category.
     """
-    return collect("spokes", lambda obj: hasattr(obj, "category") and obj.category != None and obj.category.__name__ == category)
+    return collect("pyanaconda.ui.gui.spokes.%s", os.path.dirname(__file__), lambda obj: hasattr(obj, "category") and obj.category != None and obj.category.__name__ == category)
diff --git a/pyanaconda/ui/gui/spokes/custom.py b/pyanaconda/ui/gui/spokes/custom.py
index 5932659..5a44d4a 100644
--- a/pyanaconda/ui/gui/spokes/custom.py
+++ b/pyanaconda/ui/gui/spokes/custom.py
@@ -41,7 +41,7 @@ from pyanaconda.storage import Root
 from pyanaconda.storage.partitioning import doPartitioning
 from pyanaconda.storage.errors import StorageError
 
-from pyanaconda.ui.gui import UIObject
+from pyanaconda.ui.gui import GUIObject
 from pyanaconda.ui.gui.spokes import NormalSpoke
 from pyanaconda.ui.gui.spokes.storage import StorageChecker
 from pyanaconda.ui.gui.spokes.lib.cart import SelectedDisksDialog
@@ -55,13 +55,13 @@ __all__ = ["CustomPartitioningSpoke"]
 
 new_install_name = _("New %s %s Installation") % (productName, productVersion)
 
-class AddDialog(UIObject):
+class AddDialog(GUIObject):
     builderObjects = ["addDialog"]
     mainWidgetName = "addDialog"
     uiFile = "spokes/custom.ui"
 
     def __init__(self, *args, **kwargs):
-        UIObject.__init__(self, *args, **kwargs)
+        GUIObject.__init__(self, *args, **kwargs)
         self.size = Size(bytes=0)
         self.mountpoint = ""
 
@@ -80,12 +80,12 @@ class AddDialog(UIObject):
         self.window.destroy()
 
     def refresh(self):
-        UIObject.refresh(self)
+        GUIObject.refresh(self)
 
     def run(self):
         return self.window.run()
 
-class ConfirmDeleteDialog(UIObject):
+class ConfirmDeleteDialog(GUIObject):
     builderObjects = ["confirmDeleteDialog"]
     mainWidgetName = "confirmDeleteDialog"
     uiFile = "spokes/custom.ui"
@@ -97,7 +97,7 @@ class ConfirmDeleteDialog(UIObject):
         self.window.destroy()
 
     def refresh(self, mountpoint, device):
-        UIObject.refresh(self)
+        GUIObject.refresh(self)
         label = self.builder.get_object("confirmLabel")
 
         if mountpoint:
diff --git a/pyanaconda/ui/gui/spokes/datetime_spoke.py b/pyanaconda/ui/gui/spokes/datetime_spoke.py
index 42767bb..944cdfc 100644
--- a/pyanaconda/ui/gui/spokes/datetime_spoke.py
+++ b/pyanaconda/ui/gui/spokes/datetime_spoke.py
@@ -25,7 +25,7 @@ N_ = lambda x: x
 
 from gi.repository import AnacondaWidgets, GLib, Gtk
 
-from pyanaconda.ui.gui import UIObject
+from pyanaconda.ui.gui import GUIObject
 from pyanaconda.ui.gui.spokes import NormalSpoke
 from pyanaconda.ui.gui.categories.localization import LocalizationCategory
 from pyanaconda.ui.gui.utils import enlightbox
@@ -43,13 +43,13 @@ SERVER_QUERY = 2
 
 POOL_SERVERS_NOTE = _("Note: pool servers may not be available all the time")
 
-class NTPconfigDialog(UIObject):
+class NTPconfigDialog(GUIObject):
     builderObjects = ["ntpConfigDialog", "addImage", "serversStore"]
     mainWidgetName = "ntpConfigDialog"
     uiFile = "spokes/datetime_spoke.ui"
 
     def __init__(self, *args):
-        UIObject.__init__(self, *args)
+        GUIObject.__init__(self, *args)
 
         #used to ensure uniqueness of the threads' names
         self._threads_counter = 0
diff --git a/pyanaconda/ui/gui/spokes/keyboard.py b/pyanaconda/ui/gui/spokes/keyboard.py
index 4f390d5..f0084b3 100644
--- a/pyanaconda/ui/gui/spokes/keyboard.py
+++ b/pyanaconda/ui/gui/spokes/keyboard.py
@@ -26,7 +26,7 @@ N_ = lambda x: x
 
 from gi.repository import GLib, Gkbd, Gtk
 
-from pyanaconda.ui.gui import UIObject
+from pyanaconda.ui.gui import GUIObject
 from pyanaconda.ui.gui.spokes import NormalSpoke
 from pyanaconda.ui.gui.categories.localization import LocalizationCategory
 from pyanaconda.ui.gui.utils import enlightbox
@@ -38,14 +38,14 @@ def _show_layout(column, renderer, model, itr, wrapper):
     value = wrapper.name_to_show_str[model[itr][0]]
     renderer.set_property("text", value)
 
-class AddLayoutDialog(UIObject):
+class AddLayoutDialog(GUIObject):
     builderObjects = ["addLayoutDialog", "newLayoutStore",
                       "newLayoutStoreFilter", "newLayoutStoreSort"]
     mainWidgetName = "addLayoutDialog"
     uiFile = "spokes/keyboard.ui"
 
     def __init__(self, *args):
-        UIObject.__init__(self, *args)
+        GUIObject.__init__(self, *args)
         self._xkl_wrapper = keyboard.XklWrapper.get_instance()
 
     def matches_entry(self, model, itr, user_data=None):
@@ -357,4 +357,3 @@ class KeyboardSpoke(NormalSpoke):
             layouts_list.append(row[0])
 
         self._xkl_wrapper.replace_layouts(layouts_list)
-
diff --git a/pyanaconda/ui/gui/spokes/lib/cart.py b/pyanaconda/ui/gui/spokes/lib/cart.py
index 54f2acc..dbbfc1d 100644
--- a/pyanaconda/ui/gui/spokes/lib/cart.py
+++ b/pyanaconda/ui/gui/spokes/lib/cart.py
@@ -21,7 +21,7 @@
 
 from gi.repository import Gtk
 
-from pyanaconda.ui.gui import UIObject
+from pyanaconda.ui.gui import GUIObject
 from pyanaconda.storage.size import Size
 
 import gettext
@@ -39,7 +39,7 @@ def size_str(mb):
 
     return str(Size(spec=spec)).upper()
 
-class SelectedDisksDialog(UIObject):
+class SelectedDisksDialog(GUIObject):
     builderObjects = ["selected_disks_dialog", "disk_store"]
     mainWidgetName = "selected_disks_dialog"
     uiFile = "spokes/lib/cart.ui"
diff --git a/pyanaconda/ui/gui/spokes/network.py b/pyanaconda/ui/gui/spokes/network.py
index b11fb4c..fb68c3f 100644
--- a/pyanaconda/ui/gui/spokes/network.py
+++ b/pyanaconda/ui/gui/spokes/network.py
@@ -34,7 +34,7 @@
 
 from gi.repository import Gtk, AnacondaWidgets
 
-from pyanaconda.ui.gui import UIObject
+from pyanaconda.ui.gui import GUIObject
 from pyanaconda.ui.gui.spokes import NormalSpoke, StandaloneSpoke
 from pyanaconda.ui.gui.categories.software import SoftwareCategory
 from pyanaconda.ui.gui.hubs.summary import SummaryHub
diff --git a/pyanaconda/ui/gui/spokes/source.py b/pyanaconda/ui/gui/spokes/source.py
index 68020c8..c8c708d 100644
--- a/pyanaconda/ui/gui/spokes/source.py
+++ b/pyanaconda/ui/gui/spokes/source.py
@@ -32,7 +32,7 @@ import os.path
 from gi.repository import AnacondaWidgets, GLib, Gtk
 
 from pyanaconda.image import opticalInstallMedia, potentialHdisoSources
-from pyanaconda.ui.gui import UIObject, communication
+from pyanaconda.ui.gui import GUIObject, communication
 from pyanaconda.ui.gui.spokes import NormalSpoke
 from pyanaconda.ui.gui.categories.software import SoftwareCategory
 from pyanaconda.ui.gui.utils import enlightbox, gdk_threaded
@@ -45,7 +45,7 @@ MOUNTPOINT = "/mnt/install/isodir"
 METADATA_DOWNLOAD_MESSAGE = _("Downloading package metadata...")
 METADATA_ERROR_MESSAGE = _("Error downloading package metadata...")
 
-class ProxyDialog(UIObject):
+class ProxyDialog(GUIObject):
     builderObjects = ["proxyDialog"]
     mainWidgetName = "proxyDialog"
     uiFile = "spokes/source.ui"
@@ -89,7 +89,7 @@ class ProxyDialog(UIObject):
     def refresh(self):
         import re
 
-        UIObject.refresh(self)
+        GUIObject.refresh(self)
 
         self._proxyCheck = self.builder.get_object("enableProxyCheck")
         self._proxyInfoBox = self.builder.get_object("proxyInfoBox")
@@ -126,7 +126,7 @@ class ProxyDialog(UIObject):
     def run(self):
         self.window.run()
 
-class MediaCheckDialog(UIObject):
+class MediaCheckDialog(GUIObject):
     builderObjects = ["mediaCheckDialog"]
     mainWidgetName = "mediaCheckDialog"
     uiFile = "spokes/source.ui"
@@ -193,13 +193,13 @@ class MediaCheckDialog(UIObject):
 #     result from run(), the file path you use is relative to the root of the
 #     mounted partition.  In other words, it will not contain the
 #     "/mnt/isodir/install" part.  This is consistent with the rest of anaconda.
-class IsoChooser(UIObject):
+class IsoChooser(GUIObject):
     builderObjects = ["isoChooserDialog", "isoFilter"]
     mainWidgetName = "isoChooserDialog"
     uiFile = "spokes/source.ui"
 
     def refresh(self, currentFile=""):
-        UIObject.refresh(self)
+        GUIObject.refresh(self)
         self._chooser = self.builder.get_object("isoChooser")
         self._chooser.connect("current-folder-changed", self.on_folder_changed)
         self._chooser.set_filename(MOUNTPOINT + "/" + currentFile)
@@ -235,7 +235,7 @@ class IsoChooser(UIObject):
         if not d.startswith(MOUNTPOINT):
             chooser.set_current_folder(MOUNTPOINT)
 
-class AdditionalReposDialog(UIObject):
+class AdditionalReposDialog(GUIObject):
     builderObjects = ["additionalReposDialog", "peopleRepositories", "peopleRepositoriesFilter"]
     mainWidgetName = "additionalReposDialog"
     uiFile = "spokes/source.ui"
@@ -243,7 +243,7 @@ class AdditionalReposDialog(UIObject):
     typingTimeout = 1
 
     def __init__(self, *args, **kwargs):
-        UIObject.__init__(self, *args, **kwargs)
+        GUIObject.__init__(self, *args, **kwargs)
 
         self._filterTimer = None
         self._urlTimer = None
@@ -279,7 +279,7 @@ class AdditionalReposDialog(UIObject):
         self._sourceSelectionUrl = self.builder.get_object("addRepositorySelectUrl")
 
     def refresh(self, currentFile=""):
-        UIObject.refresh(self)
+        GUIObject.refresh(self)
 
     def run(self):
         retval = None
diff --git a/pyanaconda/ui/gui/spokes/storage.py b/pyanaconda/ui/gui/spokes/storage.py
index 7a156a4..ec26f76 100644
--- a/pyanaconda/ui/gui/spokes/storage.py
+++ b/pyanaconda/ui/gui/spokes/storage.py
@@ -41,7 +41,7 @@
 from gi.repository import Gdk, Gtk
 from gi.repository import AnacondaWidgets
 
-from pyanaconda.ui.gui import UIObject, communication
+from pyanaconda.ui.gui import GUIObject, communication
 from pyanaconda.ui.gui.spokes import NormalSpoke
 from pyanaconda.ui.gui.spokes.lib.cart import SelectedDisksDialog
 from pyanaconda.ui.gui.categories.storage import StorageCategory
@@ -107,7 +107,7 @@ def size_str(mb):
 
     return str(Size(spec=spec)).upper()
 
-class InstallOptions1Dialog(UIObject):
+class InstallOptions1Dialog(GUIObject):
     builderObjects = ["options1_dialog"]
     mainWidgetName = "options1_dialog"
     uiFile = "spokes/storage.ui"
diff --git a/pyanaconda/ui/tui/__init__.py b/pyanaconda/ui/tui/__init__.py
index ba6f515..e1a36f5 100644
--- a/pyanaconda/ui/tui/__init__.py
+++ b/pyanaconda/ui/tui/__init__.py
@@ -16,6 +16,9 @@ class ErrorDialog(tui.UIScreen):
         text = tui.TextWidget(self._message)
         self.window.append(tui.CenterWidget(text))
 
+    def prompt(self):
+        return u"Press enter to exit."
+
     def input(self, key):
         self.close()
 
@@ -114,3 +117,29 @@ class TextUserInterface(ui.UserInterface):
         question_window = YesNoDialog(message)
         self._app.switch_window_modal(question_window)
         return question_window.answer
+
+class TUIObject(tui.UIScreen, common.UIObject):
+    title = u"Default title"
+
+    def __init__(self, app):
+        tui.UIScreen.__init__(self, app)
+        common.UIObject.__init__(self)
+
+    @property
+    def showable(self):
+        return True
+
+    def teardown(self):
+        pass
+
+    def initialize(self):
+        pass
+
+    def refresh(self):
+        """Put everything to display into self.window list."""
+        pass
+
+    def retranslate(self):
+        # do retranslation stuff
+        # redraw
+        self.app.switch_screen(self)
diff --git a/pyanaconda/ui/tui/common/__init__.py b/pyanaconda/ui/tui/common/__init__.py
deleted file mode 100644
index c616d9f..0000000
--- a/pyanaconda/ui/tui/common/__init__.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from .. import simpleline as tui
-
-class UIObject(tui.UIScreen):
-    title = u"Default title"
-
-    def __init__(self, app, data):
-        tui.UIScreen.__init__(self, app, data)
-
-    @property
-    def showable(self):
-        return True
-
-    def teardown(self):
-        pass
-
-    def initialize(self):
-        pass
-
-    def refresh(self):
-        """Put everything to display into self.window list."""
-        pass
-
-    def retranslate(self):
-        # do retranslation stuff
-        # redraw
-        self.app.switch_screen(self)
diff --git a/pyanaconda/ui/tui/hubs/__init__.py b/pyanaconda/ui/tui/hubs/__init__.py
index 2cb081c..a4343d4 100644
--- a/pyanaconda/ui/tui/hubs/__init__.py
+++ b/pyanaconda/ui/tui/hubs/__init__.py
@@ -1,12 +1,12 @@
 from .. import simpleline as tui
-from .. import common
+from pyanaconda.ui.tui import TUIObject
 
-class TUIHub(common.UIObject):
+class TUIHub(TUIObject):
     spokes = []
     title = "Default HUB title"
 
     def __init__(self, app, data):
-        tui.UIScreen.__init__(self, app, data)
+        TUIObject.__init__(self, app)
         self._spokes = {}
         self._spoke_count = 0
 
diff --git a/pyanaconda/ui/tui/simpleline/base.py b/pyanaconda/ui/tui/simpleline/base.py
index 7b1562a..715b097 100644
--- a/pyanaconda/ui/tui/simpleline/base.py
+++ b/pyanaconda/ui/tui/simpleline/base.py
@@ -164,9 +164,8 @@ class App(object):
 class UIScreen(object):
     title = u"Screen.."
 
-    def __init__(self, app, data):
+    def __init__(self, app):
         self._app = app
-        self._data = data
         self._window = []
 
     def refresh(self, args = None):
@@ -196,10 +195,6 @@ class UIScreen(object):
         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
 
diff --git a/pyanaconda/ui/tui/spokes/__init__.py b/pyanaconda/ui/tui/spokes/__init__.py
index 8e6075a..5727d0c 100644
--- a/pyanaconda/ui/tui/spokes/__init__.py
+++ b/pyanaconda/ui/tui/spokes/__init__.py
@@ -1,12 +1,18 @@
 from .. import simpleline as tui
-from .. import common
+from pyanaconda.ui.tui import TUIObject
+from pyanaconda.ui import common
 
-class TUISpoke(common.UIObject, tui.Widget):
+__all__ = ["TUISpoke", "StandaloneSpoke", "NormalSpoke", "PersonalizationSpoke",
+           "collect_spokes", "collect_categories"]
+
+class TUISpoke(TUIObject, tui.Widget, common.Spoke):
     title = u"Default spoke title"
+    category = u""
 
-    def __init__(self, app, data):
-        common.UIObject.__init__(self, app, data)
+    def __init__(self, app, ksdata, storage, payload, instclass):
+        TUIObject.__init__(self, app)
         tui.Widget.__init__(self)
+        common.Spoke.__init__(self, ksdata, storage, payload, instclass)
 
     @property
     def status(self):
@@ -17,8 +23,7 @@ class TUISpoke(common.UIObject, tui.Widget):
         return True
 
     def refresh(self, args = None):
-        common.UIObject.refresh(self, args)
-
+        TUIObject.refresh(self, args)
         return True
 
     def input(self, key):
@@ -30,7 +35,22 @@ class TUISpoke(common.UIObject, tui.Widget):
         c.render(width)
         self.draw(c)
 
-class StandaloneTUISpoke(TUISpoke):
-    preForHub = False
-    postForHub = False
-    title = "Standalone spoke title"
+class StandaloneTUISpoke(TUISpoke, common.StandaloneSpoke):
+    pass
+
+class NormalTUISpoke(TUISpoke, common.NormalSpoke):
+    pass
+
+class PersonalizationTUISpoke(TUISpoke, common.PersonalizationSpoke):
+    pass
+
+def collect_spokes(category):
+    """Return a list of all spoke subclasses that should appear for a given
+       category.
+    """
+    return collect("pyanaconda.ui.tui.spokes.%s", os.path.dirname(__file__), lambda obj: hasattr(obj, "category") and obj.category != None and obj.category.__name__ == category)
+
+def collect_categories():
+    classes = collect("pyanaconda.ui.tui.spokes.%s", os.path.dirname(__file__), lambda obj: hasattr(obj, "category") and obj.category != None and obj.category != "")
+    categories = set([c.category for c in classes])
+    return categories
-- 
1.7.10.4



More information about the anaconda-patches mailing list