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():
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()
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()
- 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):
"""
- 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.
+ """
+ self.refresh_menu()
+
+ def refresh_menu(self):
+ """
+ Refresh the menu and its bindings. Calls self.menu_items() to
+ get the new menu.
"""
- super(View, self).refresh()
+ # 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).
+ """
+ return [(u'Exit', self.exit, None)]
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.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 change_entry(self):
+ 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):
+ 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).
"""
- 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):
- 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):
+ self.binding_map = {}
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.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 = {}
- 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.
- 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):
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):
- 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
# function()
# Public API
-__all__= ('EditableListView','show_config')
+__all__= ('SearchableListView','show_config')