Don't use if expression, python 2.4 doesn't support that.
[matthijs/upstream/mobilegtd.git] / src / gui / gui.py
index b2656177865ccbbe4945700d11fb06a1463c2d46..5c4d0b8ee7e221ad26b087909f27ff64091dd20e 100644 (file)
@@ -62,12 +62,21 @@ def applicable_functions(obj,allowed_function_names):
     return [eval('obj.%s'%function_name) for function_name in function_names]
 
 def get_key(key_name):
-    if key_name == '':
+    if not key_name:
         key = None
     else:
         key=eval('EKey%s'%key_name)
     return key
 
+def key_shortname(key_name):
+    """ Find the one-character name for a key """
+    if not key_name:
+        return None
+    elif key_name == 'Backspace':
+        return 'C'
+    else:
+        return key_name
+
 def all_key_names():
     return filter(lambda entry:entry[0:4]=='EKey',dir(key_codes))
 def all_key_values():
@@ -93,7 +102,6 @@ def save_gui(object):
     object.old_menu = appuifw.app.menu
     object.old_exit_key_handler = appuifw.app.exit_key_handler
     object.old_title=appuifw.app.title
-    object.lock = Ao_lock()
 
 def restore_gui(object):
     appuifw.app.body = object.old_gui
@@ -101,146 +109,308 @@ def restore_gui(object):
     appuifw.app.exit_key_handler = object.old_exit_key_handler
     appuifw.app.title = object.old_title
 
+class View(object):
+    def __init__(self):
+        # Store a list of keys we bound, so we can unbind them
+        self.menu_keys = []
+        self.title = None
+        self.view = None
+        self.lock = Ao_lock()
+        self.exit_flag = False
+        super(View, self).__init__()
 
-class ListView(object):
-    def __init__(self,title):
+    def set_title(self, title):
         self.title = title
-        self.view = appuifw.Listbox(self.items(),self.change_entry)
-    
-    def change_entry(self):
-        pass
-    
+   
+    def set_view(self, view):
+        """
+        Sets the main view to be displayed (e.g., an appuifw.Listbox
+        instance).
+        """
+        self.view = view
+
     def run(self):
         self.adjustment = None
-        appuifw.app.screen=COMMON_CONFIG['screen'].encode('utf-8')
         save_gui(self)
+        appuifw.app.screen=COMMON_CONFIG['screen'].encode('utf-8')
         appuifw.app.title=self.title
         appuifw.app.body=self.view
         appuifw.app.exit_key_handler=self.exit
         try:
-            self.lock.wait()
             while not self.exit_flag:
                 self.refresh()
                 self.lock.wait()
         except:
-            pass
+            # TODO: Find out which exceptions to catch here. Catching
+            # and silencing all exceptions is not a good idea.
+            raise
         restore_gui(self)
+
     def exit(self):
         self.exit_flag = True
         self.lock.signal()
 
     def update(self,subject=None):
-        #logger.log(u'Updated %s'%repr(self))
+        """
+        Update the current view (e.g., make sure refresh is called). We
+        can't call it directly, since we're in another thread.
+        """
         if self.lock:
             self.lock.signal()
-        #pass
 
-    def index_changed(self,adjustment=None):
-        if adjustment:
-            index = self.selected_index() + adjustment
-        else:
-            index = self.selected_index()
-        if index < 0:
-            index = len(self.widgets) - 1
-        if index >= len(self.widgets):
-            index = 0
-        self.set_bindings_for_selection(index)
+    def refresh(self):
+        """
+        Called when the current view must be updated. Never call
+        directly. Subclasses should extend this method, not update.
+        """
+        self.refresh_menu()
+
+    def refresh_menu(self):
+        """
+        Refresh the menu and its bindings. Calls self.menu_items() to
+        get the new menu.
+        """
+        # Two helper functions
+        def shortcut_prefix(key_name):
+            short = key_shortname(key_name)
+            if short:
+                return '[%s]' % short
+            else:
+                return '   '
+
+        def do_entry((text, callback, key_name)):
+            key = get_key(key_name)
+            if key:
+                self.view.bind(key, callback)
+                self.menu_keys.append(key)
+            title = "%s %s" % (shortcut_prefix(key_name), text)
+            return(title, callback)
+
+        # Clear the bindings we previously added (we can't just clear
+        # all bindings, since other classes might have added other
+        # bindings)
+        for key in self.menu_keys:
+            self.view.bind(key, no_action)
+        self.menu_keys = []
+    
+        # Set the menu, and let do_entry add binds at the same time.
+        appuifw.app.menu = [do_entry(item) for item in self.menu_items()]
+
+    def menu_items(self):
+        """
+        Should return a list of menu items. Each menu item is a tuple:
+        (text, callback, shortcut key name).
+        """
+        return [(u'Exit', self.exit, None)]
+
+class ListView(View):
+    def __init__(self):
+        super(ListView, self).__init__()
+        self.current_index = None
+        self.items_cache = []
+        self.set_view(appuifw.Listbox([], self.entry_selected))
+        self.view.bind(EKeyUpArrow,lambda: self.arrow_key_pressed(-1))
+        self.view.bind(EKeyDownArrow,lambda: self.arrow_key_pressed(1))
+
+    def arrow_key_pressed(self, dir):
+        """
+        This function is called when an arrow key is pressed. Since we
+        don't get any "current list index has changed" events, we'll
+        have to create these ourselves this way.
+
+        Since the current index is only updated after the key event,
+        we'll have to adjust the index with the direction of the
+        keypress (-1 for up, +1 for down).
+        """
+        self.current_index = self.wrap_index(self.selected_index() + dir)
+        self.index_changed()
+        self.current_index = None
 
     def refresh(self):
-        appuifw.app.menu=self.get_menu_entries()
+        self.refresh_list()
+        super(ListView, self).refresh()
+    
+    def refresh_list(self):
+        """ Reload the list items. Calls items() again. """
+        # Remember which item was selected
+        selected = self.selected_item()
+        # Refresh the list
+        self.items_cache = self.items()
+        try:
+            # Try to find the selected item in the new list (based on
+            # the display text).
+            selected_index = self.items_cache.index(selected)
+        except ValueError:
+            # If the selected item is no longer present, just keep the
+            # index the same (but be careful not to fall off the end).
+            selected_index = self.clip_index(self.selected_index())
+        # Update the items in the view
+        self.view.set_list(self.items_cache, selected_index)
+
+    def run(self):
+        self.index_changed()
+        super(ListView, self).run()
+
+    def entry_selected(self):
+        """
+        This function is called when the user selects an an entry (e.g.,
+        navigates to it and push the ok button).
+        """
+        pass
+
+    def index_changed(self):
+        """
+        This function is called when the index changes. The given index
+        is the new index (don't use self.selected_index() here, since it
+        won't be correct yet!).
+        """
+        pass
+
+    def items(self):
+        """ This function should return the list of items to display.
+        See appuifw.ListBox for valid elements for this list. """
+        return []
 
     def set_index(self,index):
-        if index > len(self.widgets):
-            index = len(self.widgets)
-        if index < 0:
-            index = 0
-        self.view.set_list(self.items(),index)
+        """ Changes the currently selected item to index. """
+        self.view.set_list(self.items_cache, self.clip_index(index))
+
+    def selected_item(self):
+        """ Returns the (title of the) currently selected list item. """
+        if not self.items_cache:
+            return None # No items, so none is selected.
+        return self.items_cache[self.selected_index()]
+
 
     def selected_index(self):
-        return self.view.current()
+        """ Returns the currently selected index. """
+        if not self.current_index is None:
+            # We allow the current index to be overridden, so it can be
+            # valid during index_changed events. See arrow_key_pressed.
+            return self.current_index 
+        else:
+            return self.view.current()
 
+    def clip_index(self, index):
+        """
+        Make sure the given index fits within the bounds of this
+        list. If it doesn't, clip it off at the ends of the list (e.g,
+        -1 becomes 0).
+        """
+        max_index = len(self.items_cache) - 1
+        return max (0, min(max_index, index))
 
+    def wrap_index(self, index):
+        """
+        Make sure the given index fits within the bounds of this
+        list. If it doesn't, wrap it around (e.g., -1 becomes 5 in a
+        6-element list).
+        """
+        count = len(self.items_cache)
+        return index % count
+    
 class WidgetBasedListView(ListView):
-    def __init__(self,title):
+    def __init__(self):
+        self.binding_map = {}
         self.widgets = self.generate_widgets()
-        super(WidgetBasedListView,self).__init__(title)
-        self.exit_flag = False
+        super(WidgetBasedListView,self).__init__()
 
-    def run(self):
+    def index_changed(self):
+        self.refresh_menu()
+        super(WidgetBasedListView, self).index_changed()
+
+    def entry_selected(self):
+        self.current_widget().change()
         self.refresh()
-        self.set_bindings_for_selection(0)
-        ListView.run(self)
 
     def notify(self,object,attribute,new=None,old=None):
         self.refresh()
+
     def refresh(self):
-        self.widgets = self.generate_widgets()
-        self.redisplay_widgets()
+        self.refresh_widgets()
         super(WidgetBasedListView,self).refresh()
+    
+    def refresh_widgets(self):
+        """ Refresh the widget list. Calls generate_widgets(). """
+        self.widgets = self.generate_widgets()
+
     def redisplay_widgets(self):
-        self.set_index(self.selected_index())
+        """
+        Redisplay the widgets. Should be called if the widgets
+        themselves have changed, does not call generate_widgets again.
+        """
+        self.refresh_list()
+
     def items(self):
+        # Let ListView show each widget's text.
         return self.all_widget_texts()
+
     def all_widget_texts(self):
+        """
+        Return the widget texts as they should be displayed in the
+        list view.
+        """
         return [entry.list_repr() for entry in self.widgets]
 
-    
-
     def current_widget(self):
+        """ Returns the currently selected widget. """
         return self.widgets[self.selected_index()]
-        
 
-class KeyBindingView(object):
-    
-    def __init__(self):
-        self.binding_map = {}
-
-    def set_keybindings(self, binding_map):
+    def generate_widgets():
+        """ This function should return a list of widgets. """
+        return []
+        
+    def menu_items(self):
+        # Determine the current menu based on the methods available on
+        # the selected widget and on ourselves.
+        menu_items = []
+        for function in applicable_functions(self.current_widget(),self.binding_map)+\
+            applicable_functions(self,self.binding_map):
+            (key,description) = self.binding_map[function.__name__]
+            def do_callback():
+                function()
+                self.update()
+            menu_items.append((description, do_callback, key))
+        menu_items += super(WidgetBasedListView, self).menu_items()
+        return menu_items
+
+    def set_menu(self, binding_map):
         """
-        Set a new map of key bindings. This map maps method names to a
+        Set a new map of menu entries with hotkeys. This map maps method names to a
         tuple of keyname and description.
 
-        The method name refers to a method on the selected item, or the
-        current view.
+        Keyname is a string containing the name of the key (the
+        part after EKey, e.g., "0", "Star", etc.). Keyname can be "", in
+        which case the item has no shortcut.
 
-        Example: { 'search_item' : ('0', 'Search item') }
+        The method name refers to a method on the selected widget, or
+        the current view.
 
+        Example: { 'search_item' : ('0', 'Search item') }
         """
         self.binding_map = binding_map
 
-    def get_menu_entries(self):
-        menu_entries=[]
-        for key,key_name,description,function in self.key_and_menu_bindings(self.selected_index()):
-            if description != '':
-                if key:
-                    if key_name == 'Backspace': key_name='C'
-                    description='[%s] '%key_name +description
-                else:
-                    description='    '+description
-                menu_entries.append((description,function)) 
-        menu_entries.append((u'Exit', self.exit))
-        return menu_entries       
-    def set_bindings_for_selection(self,selected_index):
-        self.remove_all_key_bindings()
-        
-        for key,key_name,description,function in self.key_and_menu_bindings(selected_index):
-            if key:
-                self.view.bind(key,function)
-        self.view.bind(EKeyUpArrow,lambda: self.index_changed(-1))
-        self.view.bind(EKeyDownArrow,lambda: self.index_changed(1))
-        
-    def remove_all_key_bindings(self):
-        for key in all_key_values():
-            self.view.bind(key,no_action)
-
 class SearchableListView(WidgetBasedListView):
-    def __init__(self,title,entry_filters):
+    def __init__(self):
+        self.current_entry_filter_index = -1
+        self.entry_filters = []
+        self.filtered_list = lambda:[]
+        self.lock = None
+        super(SearchableListView,self).__init__()
+
+    def set_filters(self, entry_filters):
+        """
+        Set the filters that could be applied to this list. Each filter
+        can be applied in turn by calling switch_entry_filter (for
+        example from a key binding).
+
+        The entry_filters argument should be a list of filters. The
+        active filter is stored into self.filtered_list and should be
+        processed by generate_widgets in the subclass.
+        """
         self.current_entry_filter_index = 0
         self.entry_filters = entry_filters
         self.filtered_list = self.entry_filters[0]
-        self.lock = None
-        super(SearchableListView,self).__init__(title)
-
 
     def search_item(self):
         selected_item = appuifw.selection_list(self.all_widget_texts(),search_field=1)
@@ -248,35 +418,12 @@ class SearchableListView(WidgetBasedListView):
             selected_item = self.selected_index()
         self.view.set_list(self.items(),selected_item)
         self.set_bindings_for_selection(selected_item)
+
     def switch_entry_filter(self):
         self.current_entry_filter_index += 1
         self.filtered_list = self.entry_filters[self.current_entry_filter_index % len(self.entry_filters)]
         self.refresh()
 
-
-class EditableListView(SearchableListView,KeyBindingView):
-    def __init__(self,title,entry_filters):
-        KeyBindingView.__init__(self)
-        super(EditableListView, self).__init__(title,entry_filters)
-
-    def key_and_menu_bindings(self,selected_index):
-        key_and_menu_bindings=[]
-        for function in applicable_functions(self.widgets[selected_index],self.binding_map)+\
-            applicable_functions(self,self.binding_map):
-            execute_and_update_function = self.execute_and_update(function)
-            (key,description) = self.binding_map[function.__name__]
-            key_and_menu_bindings.append((get_key(key),key,description,execute_and_update_function))
-        return key_and_menu_bindings
-
-    def change_entry(self):
-        self.current_widget().change()
-        self.refresh()
-    def execute_and_update(self,function):
-        return lambda: (function(),self.refresh(),self.index_changed())
-
-    def notify(self,item,attribute,new=None,old=None):
-        self.refresh()
-
 #class DisplayableFunction:
 #    def __init__(self,display_name,function):
 #        self.display_name = display_name
@@ -287,4 +434,4 @@ class EditableListView(SearchableListView,KeyBindingView):
 #        function()
 
 # Public API
-__all__= ('EditableListView','show_config')
+__all__= ('SearchableListView','show_config')