"""Parse configuration strings to events and vice versa.
A configuration string defines events, which contain inputs. If an input or
event class has a ``name`` attribute, it is 'named' and is supported in
configuration strings (these can be found in :data:`inputs_by_name` and
:data:`evts_by_name`).
Commenting is shell syntax: the ``#`` character indicates that the rest of the
line is a comment, unless it is quoted. Whitespace outside of quotes
(including blank lines) is ignored.
Events
------
Each event line is followed by zero or more input lines which are added to that
event. Lines are made up of words, and shell-like quoting is supported. An
event line follows the form
.. code-block:: none
<type> <name> [args...]
where ``<type>`` is the class's ``name`` attribute (a key in
:data:`evts_by_name`) and ``<name>`` is the name to give to the event
(see :meth:`EventHandler.add() <engine.evt.handler.EventHandler.add>`).
``args`` depends on ``type``:
- ``button*`` events take any number of button modes from
:class:`evts.bmode <engine.evt.evts.bmode>` (``DOWN``, ``HELD``, etc.).
If ``REPEAT`` is included, the initial and repeat delays must follow these,
in seconds. If ``DBLCLICK`` is included, the double-click delay must follow,
in seconds
- other event types take no extra arguments.
Inputs
------
Input lines follow the form
.. code-block:: none
[event components...] [modifiers...] [scale*]<device> [device ID] \
[type[:input components...]] [args...]
where:
- ``event components`` are named components of the event this input is inside
to attach the input to (see
:data:`evts.evt_component_names <engine.evt.evts.evt_component_names>`). If
none are given, all components of the event are used in order.
- ``modifiers`` is zero or more whitespace-separated modifier definitions.
Each is within square ``[]`` brackets and is an input definition. The device
must match the input (see
:data:`inputs.mod_devices <engine.evt.inputs.mod_devices>`), and may be
omitted if it is the same. The device ID should be omitted, as it must be
the same as the input's. A modifier may also be one of the names in
:class:`inputs.mod <engine.evt.inputs.mod>`.
- ``scale`` is required for events of type ``'relaxis*'`` (see
:class:`RelAxis <engine.evt.evts.RelAxis>`) and no others (the ``*`` in this
argument is a literal character).
- ``device`` is the ``device`` attribute of the input class, found in
:data:`inputs_by_name`.
- ``device ID`` determines which device to listen for input from, and defaults
to ``True`` (see
:attr:`Input.device_id <engine.evt.inputs.Input.device_id>`). This is only
allowed for inputs with ``device`` ``'pad'``. This may also be a device
variable (:attr:`Input.device_var <engine.evt.inputs.Input.device_var>`) in
``<>``, eg. ``'<x>'`` for variable ``'x'``.
- ``type`` is the ``name`` attribute of the input class, found in
:data:`inputs_by_name`. It may be omitted if the given ``device`` has only
one possible ``type``.
- ``input components`` defines the components of the input to use as a
comma-separated string of indices. The number given must match up with
``event components``, and the default is all components of the input, in
order.
- ``args`` depends on ``device`` and ``type``:
- ``kbd key``, ``mouse button`` and ``pad *`` take a key/button ID. As
well as number identifiers, keys may be Pygame names (without the ``K_``
prefix) and mouse buttons may be names in
:class:`inputs.mbtn <engine.evt.inputs.mbtn>`.
- if the event is an axis or a button,
:class:`RelAxisInput <engine.evt.inputs.RelAxisInput>` subclasses take a
``boundary`` argument giving the maximum displacement of the axis from
``0``.
- if the event is a button,
:class:`AxisInput <engine.evt.inputs.AxisInput>` and
:class:`RelAxisInput <engine.evt.inputs.RelAxisInput>` subclasses take
thresholds arguments ``down`` and ``up``, giving the point at which the
button is triggered and released.
These are taken by the input classes, so see their documentation for more
details.
Examples
--------
This defines a number of input methods for a ``'walk'`` event:
.. code-block:: sh
axis walk
neg kbd LEFT
pos kbd RIGHT
# WASD
neg kbd a
pos kbd d
neg pos pad axis 0
# axis value depends on mouse position
neg pos mouse axis:0,1 100
The following defines tile-like movement (also useful for menus).
.. code-block:: sh
button4 move DOWN REPEAT .3 .2
left kbd LEFT
right kbd RIGHT
up kbd UP
down kbd DOWN
# recall that .6 .4 are axis toggle thresholds
left right pad axis 0 .6 .4
up down pad axis 1 .6 .4
# hold a button to speed up
button4 move_fast DOWN REPEAT .2 .1
left [CTRL] kbd LEFT
right [CTRL] kbd RIGHT
up [CTRL] kbd UP
down [CTRL] kbd DOWN
# modifier might be a shoulder button or something
left right [button 4] pad axis 0 .6 .4
up down [button 4] pad axis 1 .6 .4
This might be useful for moving a cursor (note that the mouse is treated
differently than for the ``axis`` event above):
.. code-block:: sh
relaxis2 move
left 5*kbd LEFT
right 5*kbd RIGHT
up 5*kbd UP
down 5*kbd DOWN
left right 5*pad axis 0
up down 5*pad axis 1
left right mouse axis:0,1
up down mouse axis:2,3
RTS-like unit selection:
.. code-block:: sh
# drag out a box to select units
button select DOWN UP
mouse button LEFT
# hold ctrl to drag out another box and add to the selection
button add DOWN UP
[CTRL] mouse button LEFT
# order selected units to do something
button action DOWN
mouse button RIGHT
Reference
---------
"""
import sys
import shlex
from StringIO import StringIO
import pygame as pg
from . import inputs, evts
#: A ``{cls.device: {cls.name: cls}}`` dict of usable named
#: :class:`Input <engine.evt.inputs.Input>` subclasses.
inputs_by_name = {}
for i in vars(inputs).values(): # copy or it'll change size during iteration
if (isinstance(i, type) and not i.__name__.startswith('_') and
issubclass(i, inputs.Input) and hasattr(i, 'name')):
inputs_by_name.setdefault(i.device, {})[i.name] = i
del i
#: A ``{cls.name: cls}`` dict of usable named
#: :class:`BaseEvent <engine.evt.evts.BaseEvent>` subclasses.
evts_by_name = dict(
(evt.name, evt) for evt in vars(evts).values()
if (isinstance(evt, type) and
(issubclass(evt, evts.BaseEvent) and hasattr(evt, 'name')))
)
_input_identifiers = {
inputs.KbdKey: lambda k: getattr(pg, 'K_' + k),
inputs.MouseButton: lambda k: getattr(inputs.mbtn, k),
inputs.PadButton: {}.__getitem__,
inputs.PadAxis: {}.__getitem__,
inputs.PadHat: {}.__getitem__
}
def _parse_input (lnum, n_components, words, scalable, device = None,
device_id = None):
# parse an input declaration line; words is non-empty; returns input
# find the device
device_i = None
for i, w in enumerate(words):
if scalable and '*' in w:
w = w[w.find('*') + 1:]
if w in inputs_by_name:
device_i = i
break
if device_i is None:
if device is None:
raise ValueError('line {0}: input declaration contains no '
'device'.format(lnum))
# else device was given, so may omit it
pre_dev = []
else:
device = words[device_i]
pre_dev = words[:device_i]
words = words[device_i + 1:]
# parse relaxis scale
scale = None
if scalable and '*' in device:
i = device.find('*')
scale_s = device[:i]
device = device[i + 1:]
if i:
try:
scale = float(scale_s)
except ValueError:
raise ValueError('line {0}: invalid scaling value'
.format(lnum))
# everything before device and before the first '[' is a component
for w_i, w in enumerate(pre_dev):
if w.startswith('['):
# found a modifier
break
else:
w_i = len(pre_dev) # else will be (len - 1)
evt_components = pre_dev[:w_i]
if not evt_components:
# use all components: let the event check for mismatches
evt_components = None
# separate out modifiers
all_mod_words = []
in_mod = False
for w in pre_dev[w_i:]:
if not in_mod:
if w.startswith('['):
# start of mod
in_mod = True
mod_words = []
w = w[1:]
else:
raise ValueError('line {0}: expected a modifier, got \'{1}\''
.format(lnum, w))
if in_mod:
if w.endswith(']'):
# end of mod
if w[:-1]:
mod_words.append(w[:-1])
all_mod_words.append(mod_words)
in_mod = False
else:
# continuation
mod_words.append(w)
if in_mod:
raise ValueError('line {0}: mod not closed'.format(lnum))
# find the name
names = inputs_by_name[device]
name_i = None
for i, w in enumerate(words):
if ':' in w:
w = w[:w.find(':')]
if w in names:
name_i = i
break
input_components = None
if name_i is None:
name = None
else:
name = words[name_i]
# parse input components
if ':' in name:
i = name.find(':')
ics_s = name[i + 1:]
name = name[:i]
if ics_s:
# comma-separated ints
try:
# int() handles whitespace fine
input_components = [int(ic) for ic in ics_s.split(',')]
except ValueError:
raise ValueError('line {0}: invalid input components'
.format(lnum))
if not name:
# name empty or entire argument omitted
if len(names) == 1:
# but there's only one choice
name = names.keys()[0]
else:
raise ValueError('line {0}: input declaration contains no name'
.format(lnum))
cls = names[name]
# only device ID preceeds name
if name_i is None or name_i == 0:
device_id = True
elif name_i == 1:
if device_id is not None:
print >> sys.stderr, 'warning: got device ID for modifier; ' \
'ignoring'
else:
if cls not in (inputs.PadButton, inputs.PadAxis, inputs.PadHat):
print >> sys.stderr, 'warning: got device ID for input ' \
'that doesn\'t support it; ignoring'
device_id = words[0]
if device_id and device_id[0] == '<' and device_id[-1] == '>':
device_id = device_id[1:-1]
else:
try:
device_id = int(device_id)
except ValueError:
raise ValueError('line {0}: invalid device ID: \'{1}\''
.format(lnum, device_id))
else:
raise ValueError('line {0}: too many arguments between device and name'
.format(lnum))
if name_i is not None:
words = words[name_i + 1:]
# now just arguments remain
if cls in (inputs.PadButton, inputs.PadAxis, inputs.PadHat):
args = [device_id]
else:
args = []
if cls in _input_identifiers:
# first is an identifier
src = _input_identifiers[cls]
if not words:
raise ValueError('line {0}: too few arguments'.format(lnum))
try:
ident = src(words[0])
except (AttributeError, KeyError):
try:
ident = int(words[0])
except ValueError:
raise ValueError('line {0}: invalid {1} code'
.format(lnum, name))
args.append(ident)
words = words[1:]
if cls in (inputs.KbdKey, inputs.MouseButton, inputs.PadButton):
# no more args
if words:
raise ValueError('line {0}: too many arguments'.format(lnum))
elif cls in (inputs.PadAxis, inputs.PadHat, inputs.MouseAxis):
if cls is inputs.MouseAxis:
# next arg is optional boundary
if words:
try:
bdy = float(words[0])
except ValueError:
raise ValueError('line {0}: invalid \'boundary\' argument'
.format(lnum))
words = words[1:]
else:
bdy = None
args.append(bdy)
# next args are optional thresholds
thresholds = []
if words:
# let the input check values/numbers of components
for w in words:
try:
thresholds.append(float(w))
except ValueError:
raise ValueError('line {0}: invalid \'threshold\' argument'
.format(lnum))
if not thresholds:
thresholds = None
args.append(thresholds)
# parse modifiers and add to args
mod_num = 1
for mod_words in all_mod_words:
if len(mod_words) == 1 and hasattr(inputs.mod, mod_words[0]):
# got a multi-modifier
mod_i = getattr(inputs.mod, mod_words[0])
mod_ics = (0,)
else:
# parse the mod's words like any other input
mod_i, mod_ecs, mod_ics = _parse_input(
'{0}[mod {1}]'.format(lnum, mod_num), 1, mod_words, False,
device, device_id
)
mod_num += 1
if (mod_ecs not in (None, (0,)) or
(mod_ics is None and mod_i.components > 1) or
(mod_ics is not None and len(mod_ics) > 1)):
raise ValueError('line {0}: modifier cannot use more '
'than one component'.format(lnum))
if mod_ics is None:
# mod_i has 1 component, so use that
mod_ics = (0,)
# now mod_ics is a length-1 sequence (can never be length-0)
args.append((mod_i, mod_ics[0]))
return ((() if scale is None else (scale,)) +
(cls(*args), evt_components, input_components))
def _parse_evthead (lnum, words):
# parse first line of an event declaration
# words is non-empty and first is guaranteed to be a valid event type
# returns (cls, name, args)
evt_type = words[0]
# get name
if len(words) < 2:
raise ValueError('line {0}: expected name for event'.format(lnum))
name = words[1]
if not name:
raise ValueError('line {0}: invalid event name: \'{0}\''.format(lnum))
words = words[2:]
# parse args according to event type
args = []
kwargs = {}
if evt_type in ('axis', 'axis2', 'relaxis', 'relaxis2'):
if words:
raise ValueError('line {0}: axis and relaxis events take no '
'arguments'.format(lnum))
elif evt_type in ('button', 'button2', 'button4'):
# args are modes, last few may be repeat/double-click delays
delays = []
for i in xrange(len(words)):
if hasattr(evts.bmode, words[i]):
args.append(getattr(evts.bmode, words[i]))
else:
# check for float
if i < len(words) - 3:
raise ValueError('line {0}: invalid event arguments'
.format(lnum))
# got one: do the last part of the loop
for w in words[i:]:
try:
delays.append(float(w))
except ValueError:
raise ValueError('line {0}: invalid event arguments'
.format(lnum))
break
# work out which delay is which
kwargs.update(dict(zip([
(),
('dbl_click_time',),
('initial_delay', 'repeat_delay'),
('dbl_click_time', 'initial_delay', 'repeat_delay'),
][len(delays)], delays)))
else:
raise ValueError('line {0}: unknown event type \'{1}\''
.format(lnum, evt_type))
return (evts_by_name[evt_type], name, args, kwargs)
[docs]def parse (config):
"""Parse an event configuration.
parse(config) -> parsed
:arg config: an open file-like object (with a ``readline`` method).
:return: ``{name: event}`` for each named
:class:`BaseEvent <engine.evt.evts.BaseEvent>` instance.
"""
parsed = {} # events
evt_cls = None
lnum = 1
while True:
line = config.readline()
if not line:
# end of file
break
words = shlex.split(line, True)
if words:
if words[0] in evts_by_name:
# new event: create and add current event
if evt_cls is not None:
parsed[evt_name] = evt_cls(*args, **kwargs)
evt_cls, evt_name, args, kwargs = _parse_evthead(lnum, words)
if evt_name in parsed:
raise ValueError('line {0}: duplicate event name: \'{1}\''
.format(lnum, evt_name))
scalable = evt_cls.name in ('relaxis', 'relaxis2')
else:
if evt_cls is None:
raise ValueError('line {0}: expected event'.format(lnum))
# input line
if issubclass(evt_cls, evts.MultiEvent):
n_cs = evt_cls.multiple * evt_cls.child.components
else:
n_cs = evt_cls.components
args.append(_parse_input(lnum, n_cs, words, scalable))
# else blank line
lnum += 1
if evt_cls is not None:
parsed[evt_name] = evt_cls(*args, **kwargs)
return parsed
[docs]def parse_s (config):
"""Parse an event configuration from a string.
parse(config) -> parsed
:arg config: the string to parse
:return: ``{name: event}`` for each named
:class:`BaseEvent <engine.evt.evts.BaseEvent>` instance.
"""
return parse(StringIO(config))