aboutsummaryrefslogtreecommitdiffstats
path: root/tagit/widgets/bindings.py
blob: 3192c4e6db1d4976758f318fb8ba6eb25219fc25 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
"""Configurable keybindings.

Part of the tagit module.
A copy of the license is provided with the project.
Author: Matthias Baumgartner, 2022
"""
# standard imports
import typing

# tagit imports
from tagit.utils import errors

# exports
__all__: typing.Sequence[str] = (
    'Binding',
    )


## code ##

class Binding(object):
    """Handle keybindings.

    A keybinding is a set of three constraints:
    * Key code
    * Inclusive modifiers
    * Exclusive modifiers

    Inclusive modifiers must be present, exclusive ones must not be present.
    Modifiers occuring in neither of the two lists are ignored.

    Modifiers are always lowercase strings. Additionally to SHIFT, CTRL and ALT,
    the modifiers "all" and "rest" can be used.
    "all" is a shortcut for all of the modifiers known.
    "rest" means all modifiers not consumed by the other list yet. "rest" can
    therefore only occur in at most one of the lists.

    Usage example:

    >>> # From settings, with PGUP w/o modifiers as default
    >>> Binding.check(evt, self.cfg("bindings", "browser", "page_prev",
    ...     default=Binding.simple(Binding.PGUP, None, Binding.mALL)))

    >>> # ESC or CTRL + SHIFT + a
    >>> Binding.check(evt, Binding.multi((Binding.ESC, ),
   ...      (97, (Binding.mCTRL, Binding.mSHIFT), Binding.mREST))))

    """

    # Modifiers
    mSHIFT      = 'shift'
    mCTRL       = 'ctrl'
    mALT        = 'alt'
    mCMD        = 'cmd'
    mALTGR      = 'altgr'
    mNUMLOCK    = 'numlock'
    mCAPSLOCK   = 'capslock'
    # Modifier specials
    mALL        = 'all'
    mREST       = 'rest'
    # Special keys
    BACKSPACE   = 8
    TAB         = 9
    ENTER       = 13
    ESC         = 27
    SPACEBAR    = 32
    DEL         = 127
    UP          = 273
    DOWN        = 274
    RIGHT       = 275
    LEFT        = 276
    INSERT      = 277
    HOME        = 278
    END         = 279
    PGUP        = 280
    PGDN        = 281
    F1          = 282
    F2          = 283
    F3          = 284
    F4          = 285
    F5          = 286
    F6          = 287
    F7          = 288
    F8          = 289
    F9          = 290
    F10         = 291
    F11         = 292
    F12         = 293
    CAPSLOCK    = 301
    RIGHT_SHIFT = 303
    LEFT_SHIFT  = 304
    LEFT_CTRL   = 305
    RIGHT_CTRL  = 306
    ALTGR       = 307
    ALT         = 308
    CMD         = 309

    @staticmethod
    def simple(code, inclusive=None, exclusive=None):
        """Create a binding constraint."""
        # handle strings
        inclusive = (inclusive, ) if isinstance(inclusive, str) else inclusive
        exclusive = (exclusive, ) if isinstance(exclusive, str) else exclusive

        # handle None, ensure tuple
        inclusive = tuple(inclusive) if inclusive is not None else tuple()
        exclusive = tuple(exclusive) if exclusive is not None else tuple()

        # handle code
        code = Binding.str_to_key(code.lower()) if isinstance(code, str) else code
        if code is None:
            raise errors.ProgrammingError('invalid key code')

        # build constraint
        return [(code, inclusive, exclusive)]

    @staticmethod
    def multi(*args):
        """Return binding for multiple constraints."""
        return [Binding.simple(*arg)[0] for arg in args]

    @staticmethod
    def from_string(string):
        mods = (Binding.mSHIFT, Binding.mCTRL, Binding.mALT, Binding.mCMD,
                Binding.mALTGR, Binding.mNUMLOCK, Binding.mCAPSLOCK)

        bindings = []
        for kcombo in (itm.strip() for itm in string.split(';')):
            strokes = [key.lower().strip() for key in kcombo.split('+')]

            # modifiers; ignore lock modifiers
            inc = [key for key in strokes if key in mods]
            inc = [key for key in inc if key not in (Binding.mNUMLOCK, Binding.mCAPSLOCK)]
            # key
            code = [key for key in strokes if key not in mods]
            if len(code) != 1:
                raise errors.ProgrammingError('there must be exactly one key code in a keybinding')
            code = Binding.str_to_key(code[0])
            if code is None:
                raise errors.ProgrammingError('invalid key code')

            bindings.append((code, tuple(inc), (Binding.mREST, )))

        return bindings

    @staticmethod
    def to_string(constraints):
        values = []
        for code, inc, exc in constraints:
            values.append(
                ' + '.join([m.upper() for m in inc] + [Binding.key_to_str(code)]))
        return '; '.join(values)

    @staticmethod
    def check(stroke, constraint):
        """Return True if *evt* matches the *constraint*."""
        code, char, modifiers = stroke
        all_ = {Binding.mSHIFT, Binding.mCTRL, Binding.mALT, Binding.mCMD, Binding.mALTGR}
        for key, inclusive, exclusive in constraint:
            inclusive, exclusive = set(inclusive), set(exclusive)

            if key in (code, char): # Otherwise, we don't have to process the modifiers
                # Handle specials
                if 'all' in inclusive:
                    inclusive = all_
                if 'all' in exclusive:
                    exclusive = all_
                if 'rest' in inclusive:
                    inclusive = all_ - exclusive
                if 'rest' in exclusive:
                    exclusive = all_ - inclusive

                if (all([mod in modifiers for mod in inclusive]) and
                    all([mod not in modifiers for mod in exclusive])):
                    # Code and modifiers match
                    return True

        # No matching constraint found
        return False

    @staticmethod
    def key_to_str(code, default='?'):
        if isinstance(code, str):
            return code

        if 32 <= code and code <= 226 and code != 127:
            return chr(code)

        return {
            Binding.BACKSPACE   : 'BACKSPACE',
            Binding.TAB         : 'TAB',
            Binding.ENTER       : 'ENTER',
            Binding.ESC         : 'ESC',
            Binding.SPACEBAR    : 'SPACEBAR',
            Binding.DEL         : 'DEL',
            Binding.UP          : 'UP',
            Binding.DOWN        : 'DOWN',
            Binding.RIGHT       : 'RIGHT',
            Binding.LEFT        : 'LEFT',
            Binding.INSERT      : 'INSERT',
            Binding.HOME        : 'HOME',
            Binding.END         : 'END',
            Binding.PGUP        : 'PGUP',
            Binding.PGDN        : 'PGDN',
            Binding.F1          : 'F1',
            Binding.F2          : 'F2',
            Binding.F3          : 'F3',
            Binding.F4          : 'F4',
            Binding.F5          : 'F5',
            Binding.F6          : 'F6',
            Binding.F7          : 'F7',
            Binding.F8          : 'F8',
            Binding.F9          : 'F9',
            Binding.F10         : 'F10',
            Binding.F11         : 'F11',
            Binding.F12         : 'F12',
            Binding.CAPSLOCK    : 'CAPSLOCK',
            Binding.RIGHT_SHIFT : 'RIGHT_SHIFT',
            Binding.LEFT_SHIFT  : 'LEFT_SHIFT',
            Binding.LEFT_CTRL   : 'LEFT_CTRL',
            Binding.RIGHT_CTRL  : 'RIGHT_CTRL',
            Binding.ALTGR       : 'ALTGR',
            Binding.ALT         : 'ALT',
            Binding.CMD         : 'CMD',
        }.get(code, default)

    @staticmethod
    def str_to_key(char, default=None):
        if isinstance(char, int):
            return char

        try:
            # check if ascii
            code = ord(char)
            if 32 <= code and code <= 226:
                return code
        except TypeError:
            pass

        return {
            'BACKSPACE'         : Binding.BACKSPACE,
            'TAB'               : Binding.TAB,
            'ENTER'             : Binding.ENTER,
            'ESC'               : Binding.ESC,
            'SPACEBAR'          : Binding.SPACEBAR,
            'DEL'               : Binding.DEL,
            'UP'                : Binding.UP,
            'DOWN'              : Binding.DOWN,
            'RIGHT'             : Binding.RIGHT,
            'LEFT'              : Binding.LEFT,
            'INSERT'            : Binding.INSERT,
            'HOME'              : Binding.HOME,
            'END'               : Binding.END,
            'PGUP'              : Binding.PGUP,
            'PGDN'              : Binding.PGDN,
            'F1'                : Binding.F1,
            'F2'                : Binding.F2,
            'F3'                : Binding.F3,
            'F4'                : Binding.F4,
            'F5'                : Binding.F5,
            'F6'                : Binding.F6,
            'F7'                : Binding.F7,
            'F8'                : Binding.F8,
            'F9'                : Binding.F9,
            'F10'               : Binding.F10,
            'F11'               : Binding.F11,
            'F12'               : Binding.F12,
            'CAPSLOCK'          : Binding.CAPSLOCK,
            'RIGHT_SHIFT'       : Binding.RIGHT_SHIFT,
            'LEFT_SHIFT'        : Binding.LEFT_SHIFT,
            'LEFT_CTRL'         : Binding.LEFT_CTRL,
            'RIGHT_CTRL'        : Binding.RIGHT_CTRL,
            'ALTGR'             : Binding.ALTGR,
            'ALT'               : Binding.ALT,
            'CMD'               : Binding.CMD,
        }.get(char, default)

## EOF ##