Add ListView.selected_item helper.
[matthijs/upstream/mobilegtd.git] / src / gui / gui.py
index 30ae4e2745bd6290ac47487bfdffca6d57c748f9..3d4e6fb9e605627ad789dcbe7ce00b4398c88b35 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):
     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
 
         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():
 def all_key_names():
     return filter(lambda entry:entry[0:4]=='EKey',dir(key_codes))
 def all_key_values():
@@ -102,6 +111,8 @@ def restore_gui(object):
 
 class View(object):
     def __init__(self):
 
 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.title = None
         self.view = None
         self.lock = Ao_lock()
@@ -131,137 +142,227 @@ class View(object):
                 self.refresh()
                 self.lock.wait()
         except:
                 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()
         restore_gui(self)
 
     def exit(self):
         self.exit_flag = True
         self.lock.signal()
-        super(View, self).exit()
+
+    def update(self,subject=None):
+        """
+        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()
 
     def refresh(self):
         """
 
     def refresh(self):
         """
-        Update the gui after a change in model or some user interaction.
-        Should be filled by subclasses.
+        Called when the current view must be updated. Never call
+        directly. Subclasses should extend this method, not update.
+        """
+        pass
+
+    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)
+            return '[%s]' % short if short else '   '
+
+        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).
         """
         """
-        super(View, self).refresh()
+        return [(u'Exit', self.exit, None)]
 
 class ListView(View):
     def __init__(self):
         super(ListView, self).__init__()
 
 class ListView(View):
     def __init__(self):
         super(ListView, self).__init__()
-        self.set_view(appuifw.Listbox(self.items(),self.change_entry))
+        self.current_index = None
+        self.set_view(appuifw.Listbox(self.items(),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 run(self):
+        self.index_changed()
+        super(ListView, self).run()
 
 
-    def change_entry(self):
+    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).
         """
         """
         This function is called when the user selects an an entry (e.g.,
         navigates to it and push the ok button).
         """
-        super(ListView).change_entry()
-    
-    def update(self,subject=None):
-        #logger.log(u'Updated %s'%repr(self))
-        if self.lock:
-            self.lock.signal()
-        #pass
+        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 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 refresh(self):
-        appuifw.app.menu=self.get_menu_entries()
+    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):
 
     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(),index % len(self.items()))
+
+    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):
 
     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):
 class WidgetBasedListView(ListView):
     def __init__(self):
+        self.binding_map = {}
         self.widgets = self.generate_widgets()
         super(WidgetBasedListView,self).__init__()
 
         self.widgets = self.generate_widgets()
         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.refresh()
-        self.set_bindings_for_selection(0)
-        ListView.run(self)
 
     def notify(self,object,attribute,new=None,old=None):
         self.refresh()
 
     def notify(self,object,attribute,new=None,old=None):
         self.refresh()
+
     def refresh(self):
         self.widgets = self.generate_widgets()
         self.redisplay_widgets()
         super(WidgetBasedListView,self).refresh()
     def refresh(self):
         self.widgets = self.generate_widgets()
         self.redisplay_widgets()
         super(WidgetBasedListView,self).refresh()
+
     def redisplay_widgets(self):
     def redisplay_widgets(self):
+        """
+        Redisplay the widgets. Should be called if the widgets
+        themselves have changed, does not call generate_widgets again.
+        """
         self.set_index(self.selected_index())
         self.set_index(self.selected_index())
+
     def items(self):
     def items(self):
+        # Let ListView show each widget's text.
         return self.all_widget_texts()
         return self.all_widget_texts()
+
     def all_widget_texts(self):
     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]
 
         return [entry.list_repr() for entry in self.widgets]
 
-    
-
     def current_widget(self):
     def current_widget(self):
+        """ Returns the currently selected widget. """
         return self.widgets[self.selected_index()]
         return self.widgets[self.selected_index()]
-        
 
 
-class KeyBindingView(object):
-    
-    def __init__(self):
-        self.binding_map = {}
-        super(KeyBindingView,self).__init__()
-
-    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.
 
         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
 
         """
         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):
         self.current_entry_filter_index = -1
 class SearchableListView(WidgetBasedListView):
     def __init__(self):
         self.current_entry_filter_index = -1
@@ -296,29 +397,6 @@ class SearchableListView(WidgetBasedListView):
         self.filtered_list = self.entry_filters[self.current_entry_filter_index % len(self.entry_filters)]
         self.refresh()
 
         self.filtered_list = self.entry_filters[self.current_entry_filter_index % len(self.entry_filters)]
         self.refresh()
 
-
-class EditableListView(SearchableListView,KeyBindingView):
-    def __init__(self):
-        super(EditableListView, self).__init__()
-
-    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
 #class DisplayableFunction:
 #    def __init__(self,display_name,function):
 #        self.display_name = display_name
@@ -329,4 +407,4 @@ class EditableListView(SearchableListView,KeyBindingView):
 #        function()
 
 # Public API
 #        function()
 
 # Public API
-__all__= ('EditableListView','show_config')
+__all__= ('SearchableListView','show_config')