# HG changeset patch # User Goffi # Date 1409842212 -7200 # Node ID 9f683df69a4c09ee2362852d85243a24ccac236e # Parent 12b5b1435e1731649d5227da2c8dbf184998363d shortcut keys are now managed in separate module, with a class checking for conflicts diff -r 12b5b1435e17 -r 9f683df69a4c urwid_satext/keys.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/urwid_satext/keys.py Thu Sep 04 16:50:12 2014 +0200 @@ -0,0 +1,149 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Urwid SàT extensions +# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +"""This module manage action <==> key mapping and can be extended to add new actions""" + + +class ConflictError(Exception): + pass + + +class ActionMap(dict): + """Object which manage mapping betwwen actions and keys""" + + def __init__(self, source_dict=None): + """ Initialise the map + + @param source_dict: dictionary-like object with actions to import + """ + self._namespaces_actions = {} # key = namespace, values (set) = actions + self._close_namespaces = tuple() + self._alway_check_namespaces = None + if source_dict is not None: + self.update(source_dict) + + def __setitem__(self, action, shortcut): + """set an action avoiding conflicts + + @param action (str,tuple): either an action (str) or a (namespace, action) tuple. action without namespace will not be checked by cbeck_namespaces(). namespace can also be a tuple itself, the action will the be assigned to several namespaces. + @param shortcut (str): key shortcut for this action + @raise: ConflictError if the action already exists + """ + if isinstance(action, tuple): + namespaces, action = action + if not isinstance(namespaces, tuple): + namespaces = (namespaces,) + for namespace in namespaces: + namespace_map = self._namespaces_actions.setdefault(namespace.lower(), set()) + namespace_map.add(action) + + if action in self: + raise ConflictError("The action [{}] already exists".format(action)) + return super(ActionMap, self).__setitem__(action, shortcut.lower()) + + def __delitem__(self, action): + # we don't want to delete actions + raise NotImplementedError + + def update(self, dict_like): + """Update actions with an other dictionary + + @param dict_like: dictionary like object to update actions + @raise: ConflictError if at least one of the new actions already exists + """ + if not isinstance(dict_like, dict): + raise ValueError("only dictionary subclasses are accepted for update") + conflict = dict_like.viewkeys() & self.viewkeys() + if conflict: + raise ConflictError("The actions [{}] already exists".format(','.join(conflict))) + for action, shortcut in dict_like.iteritems(): + self[action] = shortcut + + def set_close_namespaces(self, close_namespaces, always_check=None): + """Set namespaces where conflicting shortcut should not happen + + used by check_namespaces to see if the same shortcut is not used in two close namespaces (e.g. 'tab' used in edit_bar and globally) + @param close_namespaces (tuple of tuples): tuple indicating namespace where shortcut should not conflict. e.g.: (('global', 'edit'), ('confirm', 'popup', 'global')) indicate that shortcut in 'global' and 'edit' should not be the same, nor the ones between 'confirm', 'popup' and 'global'. + @param always_check (tuple): if not None, these namespaces will be close to every other ones (useful for global namespace) + """ + assert isinstance(close_namespaces, tuple) + if always_check is not None: + assert isinstance(always_check, tuple) + self._close_namespaces = close_namespaces + self._alway_check_namespaces = always_check + + def check_namespaces(self): + """Check that shortcuts are not conflicting in close namespaces""" + # we first check each namespace individually + checked = set() + + def check_namespaces(namespaces): + # for each namespace which save keys used + # if 1 key is used several times, we raise + # a ConflictError + set_shortcuts = {} + + to_check = set(namespaces + self._alway_check_namespaces) + + for namespace in to_check: + checked.add(namespace) + for action in self._namespaces_actions[namespace]: + shortcut = self[action] + if shortcut in set_shortcuts: + set_namespace = set_shortcuts[shortcut] + if set_namespace == namespace: + msg = 'shortcut [{}] is not unique in namespace "{}"'.format(shortcut, namespace) + else: + msg = 'shortcut [{}] is used both in namespaces "{}" and "{}"'.format(shortcut, set_namespace, namespace) + raise ConflictError(msg) + set_shortcuts[shortcut] = namespace + + # we first check close namespaces + for close_namespaces in self._close_namespaces: + check_namespaces(close_namespaces) + + # then the remaining ones + for namespace in set(self._namespaces_actions.keys()).difference(checked): + check_namespaces((namespace,)) + + +keys = { + ("edit", "EDIT_HOME"): 'ctrl a', + ("edit", "EDIT_END"): 'ctrl e', + ("edit", "EDIT_DELETE_TO_END"): 'ctrl k', + ("edit", "EDIT_DELETE_LAST_WORD"): 'ctrl w', + ("edit", "EDIT_ENTER"): 'enter', + ("edit", "EDIT_COMPLETE"): 'shift tab', + (("edit", "modal"), "MODAL_ESCAPE"): 'esc', + ("selectable", "TEXT_SELECT"): ' ', + ("selectable", "TEXT_SELECT2"): 'enter', + ("menu_box", "MENU_BOX_UP"): 'up', + ("menu_box", "MENU_BOX_LEFT"): 'left', + ("menu_box", "MENU_BOX_RIGHT"): 'right', + ("menu", "MENU_DOWN"): 'down', + ("menu", "MENU_UP"): 'up', + ("menu_roller", "MENU_ROLLER_UP"): 'up', + ("menu_roller", "MENU_ROLLER_DOWN"): 'down', + ("menu_roller", "MENU_ROLLER_RIGHT"): 'right', + ("columns_roller", "COLUMNS_ROLLER_LEFT"): 'left', + ("columns_roller", "COLUMNS_ROLLER_RIGHT"): 'right', + ("focus", "FOCUS_SWITCH"): 'tab', + } + +action_key_map = ActionMap(keys) diff -r 12b5b1435e17 -r 9f683df69a4c urwid_satext/sat_widgets.py --- a/urwid_satext/sat_widgets.py Wed Sep 03 17:42:07 2014 +0200 +++ b/urwid_satext/sat_widgets.py Thu Sep 04 16:50:12 2014 +0200 @@ -23,6 +23,7 @@ utf8decode = lambda s: encodings.codecs.utf_8_decode(s)[0] from urwid.util import is_mouse_press #XXX: is_mouse_press is not included in urwid in 1.0.0 +from .keys import action_key_map as a_key class AdvancedEdit(urwid.Edit): @@ -50,21 +51,21 @@ def keypress(self, size, key): #TODO: insert mode is not managed yet - if key == 'ctrl a': + if key == a_key['EDIT_HOME']: key = 'home' - elif key == 'ctrl e': + elif key == a_key['EDIT_END']: key = 'end' - elif key == 'ctrl k': + elif key == a_key['EDIT_DELETE_TO_END']: self._delete_highlighted() self.set_edit_text(self.edit_text[:self.edit_pos]) - elif key == 'ctrl w': + elif key == a_key['EDIT_DELETE_LAST_WORD']: before = self.edit_text[:self.edit_pos] pos = before.rstrip().rfind(" ")+1 self.set_edit_text(before[:pos] + self.edit_text[self.edit_pos:]) self.set_edit_pos(pos) - elif key == 'enter': + elif key == a_key['EDIT_ENTER']: self._emit('click') - elif key == 'shift tab': + elif key == a_key['EDIT_COMPLETE']: try: before = self.edit_text[:self.edit_pos] if self.completion_data: @@ -154,7 +155,7 @@ super(ModalEdit, self).setCompletionMethod(lambda text,data: callback(text, data, self._mode)) def keypress(self, size, key): - if key == 'esc': + if key == a_key['MODAL_ESCAPE']: self.mode = "NORMAL" return if self._mode == 'NORMAL' and key in self._modes: @@ -262,7 +263,7 @@ return True def keypress(self, size, key): - if key==' ' or key=='enter': + if key in (a_key['TEXT_SELECT'], a_key['TEXT_SELECT2']): self.setState(not self.__selected) else: return key @@ -695,10 +696,10 @@ return self.selected def keypress(self, size, key): - if key=='up': + if key==a_key['MENU_BOX_UP']: if self.listBox.get_focus()[1] == 0: self.parent.keypress(size, key) - elif key=='left' or key=='right': + elif key in (a_key['MENU_BOX_LEFT'], a_key['MENU_BOX_RIGHT']): self.parent.keypress(size,'up') self.parent.keypress(size,key) return super(MenuBox,self).keypress(size,key) @@ -756,9 +757,9 @@ self.loop.widget = urwid.Overlay(urwid.AttrMap(menu_box,'menubar'),self.save_bottom,('fixed left', columns),max_len+2,('fixed top',1),None) def keypress(self, size, key): - if key == 'down': + if key == a_key['MENU_DOWN']: key = 'enter' - elif key == 'up': + elif key == a_key['MENU_UP']: if self.save_bottom: self.loop.widget = self.save_bottom self.save_bottom = None @@ -801,7 +802,7 @@ callback = menu_item[1] break if callback: - self.keypress(None,'up') + self.keypress(None, a_key['MENU_UP']) callback((category, item)) def onCategoryClick(self, button): @@ -838,17 +839,17 @@ self.columns.contents[1] = (current_menu, ('weight', 1, False)) def keypress(self, size, key): - if key=='up': + if key==a_key['MENU_ROLLER_UP']: if self.columns.get_focus_column()==0 and self.selected > 0: self.selected -= 1 self._showSelected() return - elif key=='down': + elif key==a_key['MENU_ROLLER_DOWN']: if self.columns.get_focus_column()==0 and self.selected < len(self.name_list)-1: self.selected += 1 self._showSelected() return - elif key=='right': + elif key==a_key['MENU_ROLLER_RIGHT']: if self.columns.get_focus_column()==0 and \ (isinstance(self.columns.contents[1][0], urwid.Text) or \ self.menus[self.name_list[self.selected]].getMenuSize()==0): @@ -975,12 +976,12 @@ return False def keypress(self, size, key): - if key=='left': + if key==a_key['COLUMNS_ROLLER_LEFT']: if self.focus_column>0: self.focus_column-=1 self._invalidate() return - if key=='right': + if key==a_key['COLUMNS_ROLLER_RIGHT']: if self.focus_column