comparison urwid_satext/keys.py @ 84:9f683df69a4c

shortcut keys are now managed in separate module, with a class checking for conflicts
author Goffi <goffi@goffi.org>
date Thu, 04 Sep 2014 16:50:12 +0200
parents
children e0c8274f9b1c
comparison
equal deleted inserted replaced
83:12b5b1435e17 84:9f683df69a4c
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # Urwid SàT extensions
5 # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Lesser General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Lesser General Public License for more details.
16 #
17 # You should have received a copy of the GNU Lesser General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 """This module manage action <==> key mapping and can be extended to add new actions"""
21
22
23 class ConflictError(Exception):
24 pass
25
26
27 class ActionMap(dict):
28 """Object which manage mapping betwwen actions and keys"""
29
30 def __init__(self, source_dict=None):
31 """ Initialise the map
32
33 @param source_dict: dictionary-like object with actions to import
34 """
35 self._namespaces_actions = {} # key = namespace, values (set) = actions
36 self._close_namespaces = tuple()
37 self._alway_check_namespaces = None
38 if source_dict is not None:
39 self.update(source_dict)
40
41 def __setitem__(self, action, shortcut):
42 """set an action avoiding conflicts
43
44 @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.
45 @param shortcut (str): key shortcut for this action
46 @raise: ConflictError if the action already exists
47 """
48 if isinstance(action, tuple):
49 namespaces, action = action
50 if not isinstance(namespaces, tuple):
51 namespaces = (namespaces,)
52 for namespace in namespaces:
53 namespace_map = self._namespaces_actions.setdefault(namespace.lower(), set())
54 namespace_map.add(action)
55
56 if action in self:
57 raise ConflictError("The action [{}] already exists".format(action))
58 return super(ActionMap, self).__setitem__(action, shortcut.lower())
59
60 def __delitem__(self, action):
61 # we don't want to delete actions
62 raise NotImplementedError
63
64 def update(self, dict_like):
65 """Update actions with an other dictionary
66
67 @param dict_like: dictionary like object to update actions
68 @raise: ConflictError if at least one of the new actions already exists
69 """
70 if not isinstance(dict_like, dict):
71 raise ValueError("only dictionary subclasses are accepted for update")
72 conflict = dict_like.viewkeys() & self.viewkeys()
73 if conflict:
74 raise ConflictError("The actions [{}] already exists".format(','.join(conflict)))
75 for action, shortcut in dict_like.iteritems():
76 self[action] = shortcut
77
78 def set_close_namespaces(self, close_namespaces, always_check=None):
79 """Set namespaces where conflicting shortcut should not happen
80
81 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)
82 @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'.
83 @param always_check (tuple): if not None, these namespaces will be close to every other ones (useful for global namespace)
84 """
85 assert isinstance(close_namespaces, tuple)
86 if always_check is not None:
87 assert isinstance(always_check, tuple)
88 self._close_namespaces = close_namespaces
89 self._alway_check_namespaces = always_check
90
91 def check_namespaces(self):
92 """Check that shortcuts are not conflicting in close namespaces"""
93 # we first check each namespace individually
94 checked = set()
95
96 def check_namespaces(namespaces):
97 # for each namespace which save keys used
98 # if 1 key is used several times, we raise
99 # a ConflictError
100 set_shortcuts = {}
101
102 to_check = set(namespaces + self._alway_check_namespaces)
103
104 for namespace in to_check:
105 checked.add(namespace)
106 for action in self._namespaces_actions[namespace]:
107 shortcut = self[action]
108 if shortcut in set_shortcuts:
109 set_namespace = set_shortcuts[shortcut]
110 if set_namespace == namespace:
111 msg = 'shortcut [{}] is not unique in namespace "{}"'.format(shortcut, namespace)
112 else:
113 msg = 'shortcut [{}] is used both in namespaces "{}" and "{}"'.format(shortcut, set_namespace, namespace)
114 raise ConflictError(msg)
115 set_shortcuts[shortcut] = namespace
116
117 # we first check close namespaces
118 for close_namespaces in self._close_namespaces:
119 check_namespaces(close_namespaces)
120
121 # then the remaining ones
122 for namespace in set(self._namespaces_actions.keys()).difference(checked):
123 check_namespaces((namespace,))
124
125
126 keys = {
127 ("edit", "EDIT_HOME"): 'ctrl a',
128 ("edit", "EDIT_END"): 'ctrl e',
129 ("edit", "EDIT_DELETE_TO_END"): 'ctrl k',
130 ("edit", "EDIT_DELETE_LAST_WORD"): 'ctrl w',
131 ("edit", "EDIT_ENTER"): 'enter',
132 ("edit", "EDIT_COMPLETE"): 'shift tab',
133 (("edit", "modal"), "MODAL_ESCAPE"): 'esc',
134 ("selectable", "TEXT_SELECT"): ' ',
135 ("selectable", "TEXT_SELECT2"): 'enter',
136 ("menu_box", "MENU_BOX_UP"): 'up',
137 ("menu_box", "MENU_BOX_LEFT"): 'left',
138 ("menu_box", "MENU_BOX_RIGHT"): 'right',
139 ("menu", "MENU_DOWN"): 'down',
140 ("menu", "MENU_UP"): 'up',
141 ("menu_roller", "MENU_ROLLER_UP"): 'up',
142 ("menu_roller", "MENU_ROLLER_DOWN"): 'down',
143 ("menu_roller", "MENU_ROLLER_RIGHT"): 'right',
144 ("columns_roller", "COLUMNS_ROLLER_LEFT"): 'left',
145 ("columns_roller", "COLUMNS_ROLLER_RIGHT"): 'right',
146 ("focus", "FOCUS_SWITCH"): 'tab',
147 }
148
149 action_key_map = ActionMap(keys)